Melange
A photographer needs a model. A model needs a stylist. Right now that happens in Instagram DMs. I built an app for it — on the App Store and the web, one database behind both — and the part that took the thinking was deciding what each person sees first.
- What I proposed
- A two-sided matching app for creative collaborators, iOS and web on one Postgres, where a match exists only when both people said yes and the feed order is a single database query.
- What it produced
- Shipped and live on the App Store and the web. Mutual consent has been enforced inside the database since October 2026, after a review found it enforced only in the client. Usage so far is test accounts, and the repository's status file says so.
What a match actually is
Post what you're working on, swipe through other people's projects, and when you both say yes a chat opens. That's the product. Underneath, a match is a row that neither app is allowed to decide on its own.
| user1_id | user2_id |
|---|---|
| (0 rows) — one right swipe is not a match | |
The feed is a ranking problem
A matching app is only as good as the order it shows things in. With a handful of posts you can show everything; past that you have to decide what someone sees first, and a bad ordering kills the product before anyone notices the product.
The whole ranker is one Postgres function. Eight weighted terms, no service to call, no model to deploy — the feed is a query.
Embeddings without an inference server
One of those terms compares the text of your profile against the text of a post. The obvious way is to call an embedding API on every write, which means an API key, a bill, a network hop in the middle of a database trigger, and a failure mode where posts save but arrive unsearchable.
Instead the embedding is computed in SQL. Each word is hashed into one of 128 buckets, the counts are L2-normalised, and similarity is a cosine between two REAL[] columns. It's the hashing trick, which is older and dumber than a neural embedding and has the property that matters here: it's a pure function with no dependencies, so a trigger can maintain it on every insert and update and it can never fail separately from the write.
It doesn't understand that “filmmaker” and “cinematographer” are related, and a real embedding would. That's the trade, made deliberately: at this size the recency and role terms carry the ranking anyway, and the version that needs no key is the version that still runs in a year.
A hole in the storage rules
Auditing the schema against the live database, I found the upload policy on the media bucket checked only that you were signed in — not that you were writing to your own folder. Any logged-in user could have written into anyone else's path. Two more buckets had been created directly against production, outside migration tracking, with no ownership check at all.
All three now require the path to start with your own user ID, and the buckets are declared in the schema file so a fresh install can't silently come up with the permissive version. The interesting part isn't the fix, which is four lines. It's that the schema in the repo and the database in production had quietly drifted apart, and nothing would have told me.
What's built
Everything above works on both the phone and the web, on one shared account. Thirty automated tests run on every change.
Where it stands: 1.0 is live on the App Store. 1.1 is in Prepare for Submission — iOS build 10, screenshots captured and uploading through fastlane. Nine accounts and effectively no usage; the status doc says so in its first paragraph and gives the real numbers.