← All case studies

YesTonite - Safe link previews, maintenance mode and operations

Jevgenij - Jev - CTOJevgenij - Jev - CTO
·
CTO sprintClient projectBackendSecuritySSRFDevOpsDirectusAWS
YesTonite - Safe link previews, maintenance mode and operations

The parts of a live dating app that members never see: safe link previews in chats, a maintenance mode that keeps people signed in, and the dashboards and release pipeline that keep YesTonite running.

See YesTonite

Part 5 of the YesTonite series. New here? Start with the main case study: YesTonite - Building a casual dating app for web and Android.

 


Project snapshot

Field

Detail

Project

YesTonite: link previews, maintenance mode and operations

Type

Client project · backend engineering and operations

Client

City Dev, the Netherlands (operator of YesTonite)

Industry

Online dating and social discovery (18+)

Built

2026

Timeline

About one week to the live web app · Android in Google Play closed testing from mid-October 2026

Started

6 October 2026

Team

1 CTO, 1 developer, 1 designer

Delivery

Built end to end by techlead.fit

Status

Live in production on the web · in the Android test build

Under the hood

Directus 12 custom extension · Node.js · PostgreSQL · sharp · opentype.js · Amazon S3 · Docker on AWS Lightsail (Frankfurt) · GitHub Actions

 


YesTonite chat with a link preview card, the maintenance screen and the status checks for API, website, database and SSL

 

The situation

YesTonite is a casual dating app where one message opens a chat. People in those chats share plans: a bar, a concert, a restaurant, an event page.

A bare link in a chat is hard to judge. Is it a real place? Is it safe to open? A preview with a picture and a title answers that at a glance.

At the same time, a live product needs the unglamorous parts: a way to pause it for planned works, a way to see whether it is healthy, and a way to ship changes without breaking it.

This post covers three of those pieces.

 

The challenge

Link previews are a classic security hole. To build a preview, the server has to fetch a page that a user chose. If that fetch is not controlled, a crafted link can make the server call its own internal network or the cloud provider's metadata service. This is server-side request forgery, or SSRF.

Previews can also leak. A password reset link or a sign-in link pasted into a chat must never be fetched by anyone, not even our own server.

Downtime should not look like a crash. During planned works, members should see a clear message and stay signed in, not a wall of errors.

A small team needs to see problems first. There is no operations department. The status of the system has to be visible in the tool the team already uses.

 

Decisions before code

Sending never waits for the web. A message is delivered at once. The preview is fetched in the background and appears when it is ready.

One guarded way out. All preview fetches go through a single module that decides which addresses the server may reach. Nothing else in the backend fetches user-supplied URLs.

Store the picture ourselves. Preview images are saved in our own storage. Members never load images from third-party sites, so those sites learn nothing about who is chatting.

Maintenance is a switch in the admin, not a deploy. The operator turns it on in Directus Studio with an expected duration. It lives in the database, so it survives restarts.

Monitoring lives in Directus Studio. No separate monitoring product to learn. The status dashboard sits next to the data it describes.

 

How it works

Link previews

When a message contains a link, the backend checks a shared cache first. A known link gets its preview at once. A new link gets a placeholder, and the clients show a skeleton loader while a background job fetches the page.

The job reads the page's title, description and preview image. If the page has a preview image, it is checked, resized and stored. If not, the server generates a branded card in the app's look: the page's first usable picture blurred behind, the title set in the brand typeface, and the domain. The finished preview reaches both people in the chat through the usual real-time update.

Preview images are served only to the participants of that chat, through short-lived signed addresses.

What is never previewed

Our own confirmation, sign-in, reset and app links, the API host, and any link that looks like it carries a secret: a token, a code, a signature or a session in the address. Those stay plain links.

Maintenance mode

The operator sets the expected minutes and switches maintenance on. Within seconds, member requests get a clear "maintenance" answer with an expected end time. The web app and the Android app show a full-screen message: "We're making YesTonite better. We'll be back in about 15 minutes." Members stay signed in.

If the works take longer, the expected end moves forward by itself, so people never see a time in the past. When the switch goes off, the apps notice within 30 seconds or when they return to the foreground, and carry on where the member was.

Everything that must keep working does: payment webhooks, e-mail notifications, health checks, the admin, the public website and scheduled jobs. Admins and moderators can still use the app, so the team can test before reopening.

Status dashboard

Every 5 minutes the backend records request counts, error rates and response times, the database round trip, connections and size, and memory, CPU and event-loop delay. It also checks the public API, the website, the database and the SSL certificate of each public host, including days left before expiry. Active member time is tracked per day and platform. All of it shows in Directus Studio and is kept for 30 days.

 

The build

The SSRF guard. Only http and https on the standard ports. No credentials in the address, GET only, no cookies. The server resolves the host name itself and every resolved address must be public: private ranges, loopback, link-local (including the cloud metadata address), carrier-grade NAT, multicast, reserved and documentation ranges are all blocked, in IPv4 and IPv6, including IPv6 forms that embed an IPv4 address.

The connection then goes to exactly the address that was checked. This pins the DNS answer, so a second lookup cannot point the request somewhere else. Redirects are followed by hand, at most three, and each one is checked again. There are hard limits on time (3 seconds to connect, 8 seconds in total) and on size.

Image handling. Downloaded images are validated before processing, with a cap on pixel count against decompression bombs, and converted to a fixed 1200 by 630 size. Branded cards render text as vector outlines, because the server has no system fonts.

Limits. At most 4 fetches run at a time, with a cap of 30 new previews per member per hour. Previews refresh after 7 days. Previews no message has used for 30 days are deleted together with their image.

Deploys. Every push to the main branch is type-checked, built and sanity-checked in GitHub Actions, then uploaded to the server over SSH. The server switches to the new release, restarts the backend, applies the database migrations and runs a health check. If any step fails, it switches back to the previous release by itself. Database changes are forward-only, and a lock prevents two deploys at once.

Diagnostics. Some bugs only happen in store builds on real phones. A small diagnostics endpoint lets the app send event names and short numbers, never free text, into a log table in Studio. It is rate limited and kept for 30 days. Production logs stay at warning level, so the database is the record.

 

Testing and validation

  • Every push is type-checked and built in CI before it can reach the server.

  • The end-to-end smoke test checks that chat images are served only to participants: without sign-in the request is refused, a participant gets the image, and a signed address issued to someone else does not work.

  • Maintenance mode can be tested locally by setting a start time in the past, to see the expected end roll forward.

  • Every deploy ends with a health check, and a failed one rolls back without manual work.

 

Outcome

Members see a picture and a title for links in their chats, with no risk that a crafted link reaches inside the infrastructure and no secret links fetched.

City Dev can pause the product for planned works with a switch and an estimate, and members keep their sessions. The status of the API, website, database and certificates is visible in the same tool used to run the product. Backend releases ship on every push, with automatic rollback.

 

What we learned

Checking the address is not enough; you have to connect to the address you checked. Without pinning the DNS answer, a host name can resolve to a public address during the check and to an internal one a moment later.

Decide what is never previewed before deciding what is. The list of secret-looking links was as much work as the preview itself, and it matters more.

Maintenance mode is a product feature. A clear message with a time, and sessions that survive, turn planned works from "the app is broken" into a short wait.

Keep operations where the team already works. A status page inside Directus Studio gets looked at. A separate tool often does not.

 

Why this is a techlead.fit case study

None of these features are on the landing page. All of them decide whether a product can run safely with a small team.

We treated them as product decisions: what members should see during works, what should never be fetched, where the team will look when something is wrong, and how a change reaches production without risk.

Build the visible product.
Then build what keeps it safe and running.
Put the controls where the team already works.

 

The YesTonite series

 


Running a product with a small team?

If your product is live, or about to be, the parts users never see decide how safely and calmly you can run it.

At techlead.fit, we review the architecture, close the security gaps, and set up releases and monitoring that a small team can handle.

Relevant services: CTO Sprint · MVP Build

We are here to help

Stop guessing.
Start building with ownership.

Book a free 30-minute call.
No pitch. No commitment.
We tell you what you need and whether we can help.