2026-05-22
Building "Constellation", continued
Part one covered the bones of the application: authentication, onboarding, identity, discovery, messaging. This one is about the layer that sits underneath all of it and quietly shapes how every other feature behaves, which is privacy.
Privacy in a dating application is a strange topic to write about, because the parts of it that work well are usually invisible. A user does not notice that their last-seen time has been clamped before it left the server. They do not notice that the discover filter cannot be combined in a particular way to infer their activity. They notice when something feels safe, and they notice even more when it does not. The work is in choosing the defaults that let "feels safe" be the baseline rather than something the user has to opt in to with a settings tour.
A few specific decisions came out of thinking about that. None of them are individually large. Together they are most of what makes the application feel like it was built for this community rather than retrofitted around them.
Read Receipts as a Default
Read receipts default off. A user who actively wants their reads visible can turn them on with one tap in account settings. Anyone who never opens that screen gets the more private posture.
Default-deny is the right baseline for any product handling sensitive data, and a dating application qualifies on every axis. Choosing the more private value for every privacy setting that ships, and letting the user elect into more exposure, is a posture worth defending on its own terms regardless of the audience. Most consumer applications could do this and choose not to, because the more exposed default tends to drive the metrics product teams optimise for. Constellation is not optimising for those metrics, so the default goes the other way.
The audience sharpens the case, in a way worth naming. Polyamorous people deal with a level of external scrutiny that gets underweighted in most product designs: workplace consequences, family rejection, custody disputes in legal systems that still treat non-monogamy as a strike against fitness as a parent, immigration paperwork, and harassment from people who target the community. The privacy controls in the application exist to protect users against the outside world, not against anyone they are already in relationship with. Polyamory is built on communication, consent and disclosure between partners.
The read-receipt signal itself is also a more genuinely optional thing than the conventions of most messaging products would suggest. Plenty of people prefer the lower-stakes experience of not knowing whether a message has been seen, in both directions. Off-by-default makes that the baseline and lets the people who do want the signal opt in to giving it.
Each side's preference controls their own visibility independently. A user with read receipts on can see when the people they message read those messages, regardless of what the other person has chosen, because the other person's reads are governed by the other person's own preference, not by a negotiated handshake.
Two Kinds of Visibility
The discover feed and the search results are two different surfaces, and a user's relationship to each of them is two different things. Discover is "show me to people who might match", search is "let people who already know my name find me". They are not the same thing, and offering a single "hide me" switch that affects both at once is the wrong model.
Account settings has two toggles: hide my profile from discover, and hide my profile from search. Both default off. A user who wants to step away from being recommended to strangers can leave search on, so a friend they have started exchanging messages with off the application can still find them by name. A user who wants to be discoverable but unsearchable can go the other way. The two toggles have their own labels and their own short descriptions, because the worst version of this is two switches whose copy is so similar that the user cannot tell them apart.
Hiding Activity
Online status is the trickiest of the privacy controls, because the temptation is to solve it badly. The naive version is a toggle that, when off, hides the green "Active now" dot in the interface. That solution leaks in two specific ways. The "currently active" filter on discover and search would still let a curious ex use the application's own controls to learn whether someone is currently online, and the server would still send the real activity timestamp to the client, which means anyone watching network traffic in a developer console can recover the value the interface is supposedly hiding.
The interface is the cheapest line of defence. The server is the one that actually matters.
The fix lives in the database. The profiles table has a show_online_status column, defaulting off, and a small Postgres helper function called visible_last_seen that takes the raw timestamp and the user's preference and returns a clamped value. If the user has opted in, the real timestamp comes back. If they have not, and they were active within the last eleven minutes, the timestamp is clamped to "eleven minutes ago", which is just outside the ten-minute window that triggers the "Active now" indicator. I chose ten minutes as the window for now because live statuses are a little harder to code for an MVP and I wanted to keep it simple. Anything older than eleven minutes passes through unchanged. The social signal of "this person was around a few hours ago, you will probably hear back" survives. The live signal of "this person is sitting in the application right now" does not.
Every endpoint that returns another user's last-seen time routes through that helper, so the clamp is applied once, in one place, and you cannot accidentally forget it. The "currently active" filter on discover and search additionally requires that the target has opted in to showing online status, so the filter itself does not become a side channel for whatever the indicator is no longer showing.
One last detail that took some thinking to land on: the show_online_status column itself is sensitive. It tells you whether someone has chosen to hide their activity, which is something you can do something with even if you do not see the activity directly. So the column is stripped from response payloads before they reach the client. The interface gets the derived signal carried by the clamped timestamp. It does not get to see the underlying preference of the person being viewed.
Privacy First, Not Last
The conventional place for privacy controls in a consumer application is account settings, deep enough in the navigation that a user who is not looking for them will never find them. That placement assumes a user will eventually go looking, and most users will not. By the time they think to check, the application has already been making decisions on their behalf for weeks.
Onboarding now has a fourth step, sitting between profile creation and entering the application proper. It is titled "Privacy first", marked with a small shield icon, and it presents every privacy toggle the application has, in one card, all defaulted off. Hide my profile from discover. Hide my profile from search. Show online status. Show read receipts. Block screenshots, with an honest note that this only works on iOS and Android. Skip straight profiles. Each row has a one-line description in plain English. No jargon, no dark patterns, nothing pre-checked.
The user can leave every toggle off and tap Finish. The defaults are the defaults, the same ones they would have had without this screen existing. What changes is that the screen exists, they have seen it, and the privacy controls are framed as the first set of decisions they get to make about how they show up on the application, rather than something tucked away to be discovered later.
The Required Fields Question
Three onboarding fields changed in the same revision. Gender, zodiac, and bio are now required rather than optional.
Gender was the easy call. Every other part of the profile assumed gender existed. The chip grid was already in place, the discover filters expected the field to be populated, and leaving it blank caused odd interactions where a user would not see profiles that should have matched them, because the filter they had set was being run against an empty value on the other side. The full list of options is comprehensive, including non-binary, transgender, genderqueer, genderfluid, agender, two-spirit, and a free-text override for anyone whose identity is not on the list. Requiring a field is reasonable when the application is going to be unhelpful without it.
Zodiac came along for similar reasons. Quieter ones, but real: the filter exists, and a profile that omits zodiac sits awkwardly in a layout that expects every profile to have one.
Bio was harder. There is a school of thought that says optional fields keep onboarding fast and conversion high. There is another that says profiles with bios get more matches, which is true on every dating product I have seen data for, and that nudging users towards writing one is in their interest as much as the application's. The bio field is now required, with an inline hint above the input that reads "Profiles with a bio tend to get more matches. A couple of sentences is plenty." Required, but not punishingly so, and with the reason explained next to the field rather than discovered the hard way later.
What This All Adds Up To
Each of these decisions is small. Read receipts off by default. Two visibility toggles instead of one. A server-side clamp on activity timestamps. A privacy step at the start of onboarding rather than buried in settings. A profile that is harder to half-fill.
Stacked together, they describe a posture. The application defaults to the more private value when there is a choice to make. It expresses privacy as a set of decisions the user gets to make on day one, rather than a set of switches they have to find. It treats the server as the place where privacy is actually enforced, and the interface as a thin layer that reflects what the server has already decided to share. None of those are revolutionary ideas on their own. Holding all of them at once, on every feature, is the part that takes deliberate attention. The app is designed to be intentional.
The constellation chart, the rotating three-dimensional relationship graph that gives the application its name, is still on the way in a non-beta form. It deserves its own post, and the engineering involved is the kind that gets technical enough to be worth taking a separate run at.