Almost everyone I talked to about DateSafe suggested the same feature: let people rate their dates. Build profiles of people to avoid. Crowdsource the red flags. It sounds like safety.
I said no, and it is the most important decision in the whole product.
DateSafe
is a dating-safety app that coordinates a private plan, timed check-ins, and escalation with a trusted circle you choose, instead of building a searchable database of people to avoid.
Why won't DateSafe let people rate their dates?
DateSafe has no ratings and no profiles of people, because a searchable database of unverified claims about named individuals, with no due process, is not a safety tool. It is a weapon waiting for its first misuse.
So I wrote the rule into the spec on day one: DateSafe is private planning, trusted-circle sharing, check-ins, and user-controlled escalation. It is not a public database of anyone.
That "no" forced the reframe that made everything else work: dating safety is not information about strangers, it is coordination with people who love you. Once I believed that, the app designed itself.
The constraint you write down first is usually the thing that ends up designing the product. It is the same move that produced AbleMakers, built around makers with Down syndrome rather than around everyone.
What does a DateSafe safety session actually do?
A DateSafe safety session is one flow: you make a Date Card saying who, where and when in under 30 seconds, share it with a trusted circle who opted in by text, and start timed check-ins. If you miss one, or if something goes wrong, your circle hears about it, fast.
When you get home safe, the session ends and everyone relaxes.
The engineering interest is entirely in the words "fast" and "when something goes wrong."
The stack, and one boring decision I am glad I made
DateSafe is a pnpm monorepo: a NestJS + Prisma + Postgres API, an Expo / React Native app, a Next.js marketing site, and an admin console, all sharing one package of Zod DTOs so the app and the API physically cannot drift apart. The API runs on Railway, the app ships through Expo's EAS build pipeline to the App Store.
The decision I keep being glad about: the escalation timer is Postgres rows and a 20-second tick, not a queue. No Redis, no BullMQ. When a session starts, and after every check-in, I insert rows into an escalation_schedules table with a due_at and an action. A worker wakes up every 20 seconds and runs:
SKIP LOCKED means two workers, or a restart mid-tick, can never double-fire an alarm. Each action re-reads the live session before it does anything, so if you already marked yourself safe, it quietly no-ops. It is durable, auditable, and trivially testable with a fake clock.
For a system whose entire job is "reliably do a thing later, exactly once, even under failure," a database table beat every fancier option.
| Approach | What it costs | Best for | Where it breaks |
|---|---|---|---|
| Redis plus a job queue | Another stateful service to run and back up | High-throughput fan-out where a dropped job is survivable | The schedule lives outside your database, so "what is due" has two sources of truth |
| A cron job that scans the table | Almost nothing | Coarse work where a few minutes of drift do not matter | Minute-level granularity is too blunt for a check-in window, and overlapping runs can double-fire |
| Postgres rows on a 20-second tick with SKIP LOCKED | One worker loop and one table | Doing a thing later, exactly once, across restarts | Polling does not scale forever, and you accept a tick of latency by design |
How do you test an SMS-heavy app without a Twilio bill?
Everything external in DateSafe is a port with a fake behind it: SMS, push, key management, even the AI. In development the SMS gateway is a single line of stdout, so the whole product runs end to end with no provider account at all.
You ask for a login code and the server prints [dev sms] to +1555... Your DateSafe code is 123456. I built and tested an SMS-heavy product for weeks without a Twilio bill. In production the same port is Twilio, chosen by an environment variable.
Putting a neutral interface in front of anything you might swap is the whole thesis of ResolveMesh, which resolves an intent to a capability without caring who provides it.
The feature I am proudest of is invisible
The DateSafe duress PIN is a second passcode that ends a safety session exactly like the real one on screen, while silently telling your circle that something is wrong. To anyone watching over your shoulder, you typed a code and closed an app.
You set two PINs. The real one ends a session normally. The duress one ends it too, and looks identical doing it. It looks like a normal goodbye.
Why is a millisecond of extra latency a security bug?
In the duress path, latency is a security bug because a first date gone wrong is a close-quarters problem: the other person can see your hands and your screen, so a duress flow that runs a beat slower than the real one is a tell.
Which means the most valuable property is not volume, it is plausibility, and plausibility is a timing problem.
So the code carries a comment I am oddly proud of:
// TIMING SAFETY: the response path must match the real-PIN branch.
// The circle SMS fan-out makes sequential network calls and MUST NOT
// be awaited: the extra latency would be a visible tell to someone
// watching the phone, and a slow SMS provider would leave the user
// stuck on a spinner mid-duress.
The alert to your circle is fired and deliberately not waited on. A slow SMS provider cannot leave you staring at a spinner while someone watches. A millisecond of extra loading is, in this one place, a security bug.
The PIN screen is not the only place this rule applies. "Duress indistinguishability" is a constraint on every surface. Duress and safe-word alerts never fire a visible push, because a lock-screen banner would betray you.
The home-safe screen renders identically whether you ended normally or under duress. Even the newer Person History feature was built so it cannot look different across the real-PIN, duress-PIN and no-PIN paths. The quiet option has to be quiet everywhere, or it is not quiet at all.
Three kinds of encryption for three different problems
DateSafe uses envelope encryption, a blind index and one-way hashes, because privacy here is the schema rather than a compliance checkbox, and the three things I needed to protect have genuinely different requirements.
| Technique | What it protects | Why this one | How it works |
|---|---|---|---|
| Envelope encryption | Date details, the person's info, location points, the journal | These have to come back out in plaintext for you, so they need real reversible encryption | Each user gets a data key, and that key is wrapped by a key-management key: a local stub in dev, a cloud KMS in prod |
| Blind index | Phone numbers | An inbound safe-word text or a contact invite has to match a user by number, without the number being searchable | An HMAC of the normalized number is stored next to the ciphertext, so equality matching works with no plaintext to leak |
| One-way hashes | PINs and share-link tokens | Nothing ever needs to read these back, only verify them | PINs go through argon2; share-link tokens are stored as hashes only, and the raw token rides in the SMS link exactly once |
The rule underneath all three rows: if a value never has to be read back, do not store anything that can be.
Where does AI belong in a safety app?
In DateSafe, AI is never on the safety path. The escalation ladder and the alerts depend only on Postgres and the notification providers, and the test I care about more than any other runs every single safety flow with the AI worker offline.
The AI does non-emergency helpers behind a swappable provider port: a safety-plan drafter and a message red-flag coach. It never decides whether a person is safe, and it never triggers an emergency. Those flows are deterministic, timer-driven and boring on purpose.
Automate the mechanical part, keep the judgment out of the model's hands. That is the same line I drew building ViralVault, which publishes a tech briefing twice a day mostly automatically, and where the hard part was never the automation.
Why is every safety feature free?
Every DateSafe feature that keeps you safe is free, for everyone, forever: sessions, check-ins, the panic slider, the duress PIN, your trusted circle, one-tap 911, and full data export and delete.
I have a note from an early planning doc where one row proposed charging for panic alerts. That is not a business model. That is a ransom note with better fonts. The person most likely to need a panic button is often the person least able to pay for one.
DateSafe Plus (about six dollars a month, with a free trial, running entirely on RevenueCat) adds convenience around the safe core, never the core itself: a live watch link so your circle can follow along on a private page, an unlimited circle, the safe word by SMS, a private journal, the AI helpers, and custom check-in cadences.
Billing on a phone is a different problem from billing on the web. Everything I learned wiring Stripe into Codelit applies to a browser checkout, and on iOS the store's rules make a lot of those decisions for you, which is how RevenueCat ended up in the diagram at all.
That split also happens to be what App Review wants. The reviewer note literally has to state that the subscription adds convenience only and that every emergency feature is free, because Apple checks that for safety apps. The right thing and the shippable thing turned out to be the same thing.
The small scars
Shipping DateSafe on Apple's rails left marks. My annual product is named datesafe_plus_2_annual, with a permanent "2" in it, because I created and deleted an earlier _annual id and Apple locks a deleted product id for about a year.
On iOS the app has a home-screen widget and a Live Activity with a real ticking countdown in the Dynamic Island. On Android it is an ongoing notification, because the widget library's Android support was a stub and I was not going to block launch on it. You make a hundred of these calls and none of them are in the brochure.
Where DateSafe is now
DateSafe is at 1.0.0 and heading into the App Store, with the safety core finished and the whole thing monetized without ever putting a price on safety. Recent additions: the live watch link, Person History (cross-date memory built on a per-user hashed key so there is never a searchable profile of anyone), and a "getting home" mode for the part of the night everyone actually worries about.
There is also a private little counter on the home screen: "dates protected." It never leaves your phone and it is not analytics. It is just proof, for you, that this thing has quietly had your back a few times now.
If you build something in this shape, copy the ordering rather than the stack. The missing rating database, the fire-and-forget duress alert and the Postgres timer are the same decision wearing three different outfits: decide what the product refuses to do, then let the architecture follow.
If you are dating, or you love someone who is, DateSafe is here. Plan the date, share it with your people, and get home safe.
Questions people actually ask
- What is a duress PIN?
- A duress PIN is a second passcode that looks like your normal one but quietly signals for help. In DateSafe you set two PINs. The real one ends a safety session normally, and the duress one ends it too, looking identical on screen, while silently telling your trusted circle that something is wrong. To anyone watching over your shoulder, you typed a code and closed an app.
- Why does DateSafe not let users rate their dates?
- Because a searchable database of unverified claims about named people, with no due process, is not a safety tool. It is a weapon waiting for its first misuse. DateSafe was specified on day one as private planning, trusted-circle sharing, check-ins, and user-controlled escalation, and never as a public record of anyone. The reframe underneath it is that dating safety is coordination with people who love you, not information about strangers.
- Can you use a Postgres table instead of a job queue for scheduled alerts?
- Yes, and for DateSafe it was the better choice. Scheduled escalations are rows in a table with a due time and an action. A worker wakes every twenty seconds and selects the rows that are due, using select for update with skip locked, so two workers or a restart mid-tick can never double-fire an alarm. Each action re-reads the live session first, so if you already marked yourself safe it quietly does nothing.
- Is DateSafe free?
- Every feature that keeps you safe is free for everyone, forever. That includes safety sessions, check-ins, the panic slider, the duress PIN, your trusted circle, one-tap 911, and full data export and delete. DateSafe Plus costs about six dollars a month with a free trial and adds convenience around that core: a live watch link, an unlimited circle, the safe word by SMS, a private journal, AI helpers, and custom check-in cadences.
- Does DateSafe use AI to decide whether someone is safe?
- No. AI is never on the safety path. The escalation ladder and the alerts depend only on Postgres and the notification providers, and there is a test that runs every safety flow with the AI worker offline. The model handles non-emergency helpers behind a swappable provider port, a safety-plan drafter and a message red-flag coach. It never decides whether a person is safe and it never triggers an emergency.
- How do you store phone numbers without making them searchable?
- With a blind index. DateSafe stores an HMAC of the normalized phone number next to the encrypted value, which allows exact matching without keeping a searchable plaintext number. That is enough to route an inbound safe-word text or a contact invite to the right user. Sensitive blobs like date details and location points use envelope encryption instead, and verify-only secrets such as PINs are hashed with argon2.