Celestual lets someone place a one-way "ping" at @them and only ever reveals
anything when it is mutual. Because a person can name a non-consenting
third party, the whole design is built to leak nothing about who pinged whom —
and, since migration 0006, to make the stored data itself unreadable. This
document is the source of truth for that model; code comments reference its
§ sections. The product rationale lives in
ULTIMATE-PRODUCT-FRAMEWORK.md (esp. Part 6).
The dangerous capabilities are: "enter a handle that isn't mine and learn something about them" and "read the map of unrequited longing out of the database." Every control below exists to make both worthless.
All tables (celestual_entries, celestual_matches, celestual_notifications,
celestual_attempts, celestual_suppressions, celestual_placements,
celestual_members, celestual_handle_links, celestual_ig_verifications,
celestual_settings, celestual_communities, celestual_community_members,
celestual_campuses, celestual_campus_prereg, celestual_campus_mail) have
RLS enabled with zero policies, and all privileges are revoked from
anon/authenticated. The browser literally cannot select from them. The
only entry points are the SECURITY DEFINER RPCs (celestual_submit,
celestual_withdraw, celestual_renew, celestual_ping_status,
celestual_my_pings, celestual_slots_for, celestual_suppress,
celestual_link, celestual_set_worlds, celestual_world_counts,
celestual_campus, celestual_campus_preregister,
celestual_start_ig_verification, celestual_poll_ig_verification), which
return only small status objects — never other people's rows. Internal helpers
(celestual_group, celestual_hash_handle, celestual_is_member,
celestual_consume_ig_proof, celestual_ig_required) and the operator paths
(celestual_complete_ig_verification, celestual_campus_reveal,
celestual_purge_expired) are not granted to clients.
The server stores who a ping points at only as a salted SHA-256 hash
(to_hash; salt in celestual_settings, never client-visible). Matching runs
hash-to-hash, group-aware. Consequences, by design:
- A database dump cannot read anyone's targets. The plaintext exists only on
the sender's own device (localStorage) — and, once mutual, as
matched_handle, which both people already know. - The status page works by the device sending its own plaintext list up
(
celestual_ping_status, owner-proof-gated, capped at 10) and getting state back; the server cannot produce the list itself. - Cross-device restore (
celestual_my_pings) returns named rows only for mutual pings; unmatched pings restore as anonymous standing rows. This is a feature, not a gap. - The opt-out registry (
celestual_suppressions) is itself hashed. - The renewal email can name no handle — the server doesn't know one.
A person holds at most 3 standing (unresolved, unlapsed) pings, counted
across their identity group. Each ping stands 60 days, then lapses;
celestual_purge_expired (run hourly by celestual-remind) deletes lapsed
unmatched rows entirely — retention minimisation doing legal work (GDPR/PIPA)
as well as product work. Renewal is free and one tap (celestual_renew).
Retiring ("let it go", celestual_withdraw) frees the slot immediately.
Because retiring now frees the slot, enter→peek→retire cycling is bounded by a
placement cadence cap: at most 6 new placements per rolling 30 days
per handle (celestual_placements), on top of the hourly rate limits. Honest
use never feels it; a sweep trips it fast.
celestual_submit enforces trailing-hour caps: per-IP (40/hr),
per-from handle (20/hr), per-target (60/hr, compared by hash).
Attempt logs store the target hashed and are pruned on a rolling ~2-hour
basis. celestual_suppress is rate-limited per IP (10/hr) against mass-wipe
griefing; verification starts are capped per IP and per handle.
After (and only after) placing a ping, the sender learns whether the target is
reachable (has ever verified — celestual_members — and hasn't opted
out). Membership is the flattering receiver-side identity; still, it's a bit,
so it is guarded: no lookup without a placed ping, three slots, the cadence
cap, and the hourly limits make enumeration cost slots, time, and identity.
celestual_ping_status returns reachability only for targets the caller has
actually placed.
Unchanged from 0004 and load-bearing: a one-time 4-digit code DM'd to
@celestual.us; Meta's authenticated sender identity (relayed by ManyChat's
External Request or the direct Meta webhook — see
DEBUG-IG-WEBHOOK.md) must equal the claimed handle.
The browser mints a 256-bit proof, stores only its hash server-side, and
presents the raw proof at placement; celestual_consume_ig_proof makes the
server the authority. No match can fire to an unverified claimant — the
impersonation fix the framework calls non-negotiable (§6.5). Gated by
celestual_settings.require_ig_verification; with it on, the proof also gates
celestual_ping_status, celestual_my_pings, celestual_slots_for,
celestual_renew, celestual_set_worlds and celestual_campus_preregister.
The /demo sandbox runs the same overlay but auto-verifies locally and never
touches the backend.
A person can link up to 3 of their own @s (celestual_link); matching and the
slot count are group-aware. Claiming is first-come, never steals an @ from
another group, capped at 3. With verification enforced, the budget and claims
key on a proven identity.
celestual_submit returns mutuality instantly to the completer; the earlier
entrant is emailed only at the address they themselves stored — never the
address on the triggering request — via the celestual_notifications queue
(retry + dead-letter in celestual-notify). Withdrawal tears down the match row
and any still-pending notification, but never un-tells anyone already mailed.
A blocked/opted-out handle can never match: suppression is checked (by hash)
before anything records.
celestual_suppress is the opt-out any handle owner — user or not — can use
without an account: it hashes the handle into the block list and erases
everything referencing it (pings both directions, matches, pending mail,
membership, worlds, campus preregistrations). Free, immediate, never behind a
login, rate-limited against griefing. It is also how "delete everything" works
for a user's own handle. Reachable at /optout and documented on
/data-deletion.
Campus rows are operator-created only. Preregistration requires a verified
handle (it is the signup). The meter count is the true count. Opening at
threshold is atomic and mails everyone at once. Week-one aggregates are
snapshotted by celestual_campus_reveal (service-role, run by the
operator after eyeballing) so published numbers stay exactly true forever.
Nothing in the schema can inflate a number without lying in SQL — and nothing
may (framework §6.2: the forbidden lever).
Community counters are computed server-side and return null below 100
members (celestual_world_counts, celestual_set_worlds). Small counts both
feel empty and de-anonymize; the floor is enforced at the source of truth,
never in the client.
Three emails exist: it's mutual (to the earlier entrant's own address), your ping lapses soon (about the sender's own action; names no handle — §2), and the campus open/reveal notes (to preregistrants). None of them can state or imply anything about any other person's activity. That line is load-bearing legally (FTC v. NGL) and is pre-committed here in writing.
The landing states the 18+ condition on the primary action; marketing is college-and-up only; suspected-minor accounts are purged fast. Boring conservatism on purpose (framework §6.7).
- Instant reveal is an oracle bounded, not removed — 3 slots + 6
placements/30 days + hourly caps bound "fishing for who likes me" to a slow
trickle. If it ever proves too loose, the single lever is delaying the
completer-side reveal; the seam is isolated in
celestual_submit's return. - The salt is a secret — anyone with the service role can hash candidate
handles and test membership. Hashing protects against dumps and honest-
operator reads, not against a fully compromised operator. Encrypt at rest,
log access, treat
celestual_entriesas the crown jewels regardless. - Meta platform risk — verification rides Meta's webhook surface; keep the bio-code/ManyChat fallback maintained forever (framework §6.6).
- Pre-enforcement window — while
require_ig_verificationis'false'(dev default), identity is the typed handle. Flip it on before any real launch; the operator checklist below makes it a release gate.
- All migrations applied (
0001–0006); RLS on, zero policies, on everycelestual_*table. -
anonhas execute only on the §1 public RPC list — and not oncelestual_group,celestual_hash_handle,celestual_is_member,celestual_complete_ig_verification,celestual_consume_ig_proof,celestual_ig_required,celestual_campus_reveal,celestual_purge_expired. -
handle_saltexists incelestual_settings(0006 seeds it) and is never logged or exported. - Edge-function secrets set in Supabase, never in the front-end bundle
(
RESEND_API_KEY,CELESTUAL_FROM_EMAIL,MANYCHAT_SHARED_SECRETor the direct-Meta trio). -
celestual-remindscheduled hourly (lapse warnings + the sixty-day broom + campus mail);celestual-notifywired to the notifications insert or cron. - Only
VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY+ feature flags in the browser env; the service-role key never appears inapp/. - Release gate:
celestual_settings.require_ig_verification = 'true'before any campus window opens.