Privacy architecture
Collect almost nothing, but keep enough structure to operate safely.
The privacy model is not a slogan. It is a set of architectural constraints: no Nocturne ID document storage, no behavioral analytics by default, no persistent raw identifying logs by default, no NPM frontend dependency chain, and clear separation between payment identity, account identity, moderation evidence, and content access.
Invariant
The platform can enforce rules, revoke access, process payments, and respond to abuse without building identity dossiers on lawful users.
Technical stance
Preferred stack
| Layer | Direction | Reason |
|---|---|---|
| Backend | Rust + Axum | Small, efficient, explicit, and a strong fit for server-side sessions and service boundaries. |
| Rendering | Server-rendered templates, likely Askama or Maud | Premium HTML/CSS experience without a large frontend framework dependency surface. |
| Frontend dependencies | No NPM by default | Reduces supply-chain exposure and makes the browser payload easier to audit. |
| Styling | Handwritten CSS | Supports bespoke luxury UI without framework lock-in. |
| JavaScript | Small audited vanilla JS modules only | Enough for cinematic motion, scroll state, video affordances, and progressive enhancement. |
| Database | PostgreSQL | Durable source for accounts, entitlements, content metadata, payments, moderation, and audit events. |
| Ephemeral state | Redis later if needed | Rate limits, slow-mode counters, short-lived tokens, and temporary locks; the current foundation persists session and CSRF token digests in PostgreSQL. |
| Deployment | Single-purpose VPS with Docker isolation behind HTTPS reverse proxy | Standalone deployment boundary; explicit production-edge profile, secure-cookie/HSTS posture, backup posture, production preflight, VPS compose skeleton, Caddy/nginx edge templates, external edge smoke, and local-only diagnostics before scaling complexity. |
Account model
Pseudonymous membership and self-custody recovery
Default account fields
member_id: pseudonymous public/member identifier.invite_code_digest: SHA-256 digest of an owner-issued one-time invite; the raw invite code is shown once and never stored.credential_hash: Argon2id hash of the recovery/login secret; the plaintext key is shown once only.credential_version: supports rotation and revocation.recovery_confirmed_at: timestamp only; set after the member proves possession of the recovery key.membership_state: active, expired, locked, banned, pending payment, etc.entitlement_state: current access rights without storing rich identity.
Recovery policy
- Recovery key shown once during onboarding.
- Member-only library and media routes remain locked until the member pastes the saved recovery key and acknowledges the no-default-email recovery tradeoff.
- Nocturne verifies the submitted key against the Argon2id credential hash and stores only
recovery_confirmed_at. - Recovery-key rotation requires an active confirmed session, CSRF proof, explicit acknowledgement, and the current recovery key.
- Rotation revokes old active credential rows, inserts the next credential version, closes active sessions, deletes CSRF-token digests for those sessions, opens a fresh local session, and resets
recovery_confirmed_atuntil the replacement key is confirmed. - No default email recovery.
- If the member loses the recovery key, Nocturne cannot recover the account by default.
- Optional email recovery can be considered later, but must be explicitly opt-in.
UX requirement: the recovery tradeoff must be impossible to miss. Privacy-first means more user responsibility.
Age assurance
Pluggable age-gate abstraction
| Stage | Mechanism | Stored by Nocturne |
|---|---|---|
| Private/internal test | Self-attestation: adult-only acknowledgement and terms acceptance. | Acceptance timestamp, terms version, and account state. |
| Public launch candidate | Provider- or wallet-based proof of adult status if legally or commercially required. | Only pass/fail state, provider key, verification timestamp, expiry, and audit reference. No ID documents. |
| Future ideal | Anonymous over-18 attestation where identity stays with the user or verifier. | Minimal verification claim and expiry only. |
Invariant: Nocturne does not store ID documents. If a verifier is introduced, provider selection must prioritize deletion, minimization, auditability, and no marketing reuse.
Sessions
Server-side session model
- Use opaque server-side session IDs, not JWT-everywhere.
- Persist only a SHA-256 digest of the session ID in PostgreSQL; never store the raw cookie value.
- Store CSRF tokens as SHA-256 digests and consume them on authenticated POST actions.
- Expose only aggregate session hygiene to members: current session created/expires, active-session count, logout-current, and logout-all. Do not show or store device names, IP addresses, user-agent strings, or raw session IDs.
Log out all sessionsrevokes the member's active digest-backed session records and deletes stored CSRF-token digests for those sessions.- Credential rotation also revokes all active sessions for the member and deletes their CSRF-token digests before issuing one fresh session cookie.
- Cookie name:
__Host-session. - Cookie flags:
Secure,HttpOnly,Path=/, andSameSite=Laxor stricter where practical. - Rotate sessions on login, credential change, tier/entitlement change, suspicious event, or privilege boundary crossing.
- Initial session lifetime is thirty minutes; idle timeout and absolute timeout should become explicit configuration before public launch.
- Staff/operator sessions must be separated from member sessions. The current owner dashboard is a local MVP gate: disabled by default, unlocked only by an explicit owner key/digest, protected by CSRF on review forms, and carried by a HMAC-signed timestamped opaque cookie with configurable server-enforced expiry (
NOCTURNE_OWNER_SESSION_MAX_AGE_SECONDS, default 1800 seconds).
Privacy ledger
Data classes and retention posture
| Data class | Purpose | Retention stance | Access boundary |
|---|---|---|---|
| Account credential | Login, recovery, one-time custody confirmation, and self-custody rotation. | While account exists; rotated/revoked when changed. Confirmation keeps only a timestamp and is reset after rotation until the new key is confirmed. | System only; never plaintext after initial display, rotation display, or confirmation form handling. |
| Invite code | Controlled private-test entry before public acquisition is widened. | One-time owner-issued codes store only SHA-256 digests, optional non-identifying owner labels, timestamps, status, and pseudonymous redemption member IDs. Raw invite codes are printed once and never listed later. | Application redemption path and local owner CLI only; no email or identity binding. |
| Membership entitlement | Gate catalog and downloads. | While subscription/pass state is needed; historical events minimized. Current foundation supports preview plus local owner grant/revoke lifecycle events. | Application service and local owner CLI. |
| Payment reference | Subscription/pass state, checkout state, refunds, disputes, chargebacks. | As required by processor, tax, dispute, and accounting needs. Current foundation records only pseudonymous member ID, plan, provider label, provider checkout/order/event references, amount/currency, checkout/payment status, entitlement link, replay attempts/errors, timestamps, optional note, and optional payload digest. Browser checkout success/cancel returns are non-authoritative and never grant access by themselves; stale pending/success-returned checkout sessions can be expired, and expired sessions cannot be completed. Reversal commands and webhook replay record refund/chargeback/void events and revoke only the linked entitlement. No raw webhook payload, card data, email, IP address, user-agent, or processor customer identity is stored by default. The payment provider matrix requires written provider approval before any adapter expands this data shape. | Limited local owner CLI and member checkout skeleton now; most identity remains with processor. |
| Age-gate proof | Adult-only access evidence. | Minimal proof state and expiry; no ID documents. | System/admin audit only. |
| Operational logs | Security, rate limits, availability, abuse response. | No persistent raw identifying access logs by default; current media limits are in-memory, short-lived, and keyed by hashed session IDs. Public report abuse controls use form-body size limits, a hidden honeypot, a stateless boot-secret token, dwell/expiry checks, and an active-case queue cap instead of raw IP/user-agent tracking. Production-edge doctor/preflight checks, the current proxy templates, and external smoke preserve readiness checks without expanding app-side identity logging. | Operator-only; no analytics profiling. |
| Backup artifacts | Operational recovery and restore drills. | Backups inherit production sensitivity even though the live schema avoids email, ID documents, raw session IDs, and raw IP/user-agent logs. Store PostgreSQL dumps and private media archives encrypted/offsite, keep local artifacts short-lived, and never commit backup output to git. | Owner/operator only; restore into isolated targets before live use. |
| Moderation evidence | Incident review, bans, takedowns, required reporting. | Case-based and restricted. Current foundation stores report metadata, optional contact references/digests, optional SHA-256 evidence digests, target quarantine timestamps, and owner notes without raw IP/user-agent/email/attachments. Public /report submissions become external cases with no member ID, no public-controlled quarantine, no file upload, and non-identifying intake guardrails: 8 KiB body cap, hidden honeypot, boot-secret token, three-second dwell, 30-minute expiry, and 200 active-case queue cap; the gated /owner dashboard can review these cases without adding identity fields. | Restricted trust/safety path, public minimal intake, and gated owner dashboard. |
| Participation signals | Owner-granted voting/request allowance, polls, private requests, predefined catalog/media reactions, and aggregate unlock goals. | Account-local or aggregate only; current foundation stores pseudonymous member IDs, Signal counts, owner poll options, private request text/review notes, local Hermes screening state/rule/reason/version, aggregate reaction counts, and owner-authored unlock-goal thresholds. No public sexual behavior metrics, raw IP addresses, user-agent strings, email addresses, attachments, public profiles, leaderboards, or activity feeds. | Application, local owner CLI, and gated /owner dashboard; aggregate analytics only. |
Participation privacy
Controlled agency without a social graph
Current foundation
- Owners grant finite Signals to pseudonymous
member_idvalues. - Owners create polls and fixed options; members cannot publish poll options or public posts.
- The authenticated
/participationpage shows only owner-created open polls, aggregate option counts, active aggregate unlock goals, the member's own available Signals, and that member's recent private request statuses. - Votes require confirmed recovery custody, active entitlement, an open poll, an available
poll_voteSignal, and a valid CSRF token. - Requests require confirmed recovery custody, active entitlement, an available
content_requestSignal, a valid CSRF token, and local Hermes screening before queueing. - Request text enters a private owner queue with moderation state/rule/reason/version metadata and can be reviewed from CLI or the gated
/ownerdashboard, then accepted, declined, or archived; it is never public by default. - Reactions are predefined CSRF-protected buttons against visible catalog items or media assets; member pages show local controls only, while owner surfaces show aggregate counts.
- Unlock goals are owner-authored and compute progress from aggregate reaction counts or aggregate votes for a fixed poll option; active goals are member-visible as count/threshold progress, while owner surfaces show active/completed goal status without member IDs.
Deliberate exclusions
- No native chat, direct messages, public comments, public profiles, or leaderboards in the MVP foundation.
- No links or attachments in member request text.
- No raw IP address, user-agent, email, or device ledger attached to participation records.
- No behavior scoring that ranks members by sexual consumption or spend.
- No public display of individual votes, requests, or reactions.
Media access
No DRM, honest access control
Controls to use
- Private object storage or filesystem path outside public web root.
- Authenticated media routes or short-lived signed URLs.
- Current foundation route: invite-gated membership creation under
/join, entitlement-gated catalog pages under/libraryand/media/{asset_id}, CSRF-protected member participation actions under/participation, protected inline file responses under/media/{asset_id}/file, per-asset attachment downloads under/media/{asset_id}/downloadonly whendownload_policyallows them, provider-neutral checkout session creation under/checkoutwith success/cancel returns that never grant access plus stale checkout expiry cleanup, owner-only local invite create/list/revoke commands, owner-only local import/catalog commands that copy files into the private media root and control draft/member/archive visibility, owner-only entitlement grant/list/revoke commands, owner-only payment proof record/list/reversal commands plus provider-neutral payment-checkout and payment-webhook record/list/replay commands for payment-neutral access testing, and owner-only unlock-goal commands for aggregate reward progress. - No public bucket/object paths; storage keys are relative private paths and URL-like keys are rejected.
- Image/video playback uses authenticated disk-backed byte-range responses for browser compatibility; download links are explicit and separate from inline viewing.
- Current foundation uses ephemeral per-session media route limits keyed by SHA-256 session digests rather than IP addresses; production WAF/rate-limit tuning remains separate from the current proxy body-limit templates.
- Production-edge configuration is explicit:
NOCTURNE_DEPLOYMENT_PROFILE=production,NOCTURNE_PUBLIC_ORIGIN=https://..., secure cookies, owner gate, owner-session lifetime, andNOCTURNE_BACKUP_POSTUREare checked bydoctor; HSTS is emitted only for an HTTPS public origin. - Default form posts are capped at 16 KiB and public report posts at 8 KiB before any storage path is reached; the Caddy/nginx templates mirror those limits at the public edge.
- Detailed environment, production preflight, backup posture, checkout-lifecycle, database, filesystem, and local smoke/seed diagnostics run through local owner commands rather than public HTTP endpoints, so operational state is not disclosed to unauthenticated visitors. The browser owner dashboard is also disabled until an explicit owner key or owner-key SHA-256 digest is configured, and its cookie is a HMAC-signed timestamped opaque token with server-side expiry.
- Optional light brand watermark/end-card, not per-user forensic watermarking.
Controls to avoid
- No invasive DRM.
- No privacy-hostile per-user hidden watermarks by default.
- No promise that screenshots or screen recording can be stopped.
- No public indexing of gated assets.
Browser security
Default hardening
- Strict Content Security Policy from day one.
X-Content-Type-Options: nosniff.- Conservative
Referrer-Policy. - Minimal
Permissions-Policy. Cache-Control: no-storefor auth, recovery, payment return, and account screens.- CSRF protection for authenticated state-changing actions; current foundation covers recovery-key confirmation, recovery-key rotation, logout-current, logout-all, participation browser actions, and owner dashboard review/quarantine forms while establishing the token-digest pattern for future forms. Public
/reportremains unauthenticated by design and minimizes stored identity instead of tying reports to sessions; it uses a non-identifying stateless form token, honeypot, dwell/expiry checks, small body cap, and queue cap rather than per-reporter identity tracking.