VFLIN

docs · docs/50-cloud/DEPLOY.md#0 · synced 2026-11-08

What the server can and cannot see

read from a running unit's database: in the clear, sealed

0. What the server can and cannot see — read this before renting anything

The product’s promise is zero-knowledge custody (ADR-0014, ADR-0019): the server stores what it needs to coordinate and cannot read a profile. That was re-measured, not re-read, on 2026-09-04 by querying the database and listing the bucket after a full two-device round trip [MEASURED]:

where in the clear sealed / hashed (server cannot read)
accounts email · totp_enabled flag · created-at (S10: for an unconfirmed address, the time of its LATEST registration — a replacing registration restarts the 24-hour clock) · a 16-byte auth salt (non-secret by design) · verified_at (S8: when the address was confirmed by the mailed code; NULL = registered, not yet confirmed — cannot sign in, removed 24 h after its latest registration) · email_code_failures (the wrong-code budget, 0–5) · auth_kdf (S10, ADR-0040: the derivation version of the stored verifier — NULL = legacy, auth-v1; a label written with the verifier and announced at login-init, telling the client how to derive, never letting the server derive anything) password_hash is an Argon2id hash of a client-derived verifier, never the passphrase (ADR-0019 C1) · totp_seed_enc 60 bytes, AEAD under VFLIN_TOTP_KEY
plans (S12) the owner’s price list as DATA: code, monthly price, the ceilings (NULL = none), whether it is still sold. Not about any account; it is here because editing a price must not be a release —
email_codes (S8) relay_response + relayed_at (S12, ISSUE-204: what the mail relay answered when it took the letter — a status line and, at most relays, a queue id. About the ENVELOPE: it never contains the code, and it is what the owner hands the provider when a letter goes missing) · which account a code is for, its purpose (verify — activates the account; device — admits one new device, bound to that device’s hash), expiry (15 min), whether it was used — and an HMAC under the server secret of the 6-digit code (code_hash), never the code; a row with a NULL account is a decoy an unknown or already-confirmed address leaves at resend-code, so both branches do the same work the code itself (mailed once, single-attempt, 15 min)
devices device id (the daemon’s UUID) · device name · last-seen refresh_token_hash (43 chars; the token itself is never stored) · secret_hash (S9, 43 chars: the SHA-256 of the device secret the last sign-in handed that device — what makes it “known”; replaced by every sign-in, gone with device.revoke, NULL until the device next signs in with a mailed code; the secret itself is never stored)
profiles_meta profile id · blob key · content hash · ciphertext size · wallets (S12: how many wallets the profile holds — a number the CLIENT declares at push/commit, never one the server counts; it is what the plan’s wallet ceiling is checked against, so the ceiling bounds an honest client and not a modified one, and the contract says so) · version vector (device ids → counters) · fence (S5: the sync-lease fence of the last accepted push, a monotonic counter — coordination metadata, derived from no plaintext) · state · updated-at —
account_keyrings a JSON envelope: version, the KDF parameters (mCost/tCost/pCost), saltKek, the device-key registry (device id → 32-byte public signing key), accountPub wrappedMkPassword, wrappedMkRecovery, wrappedAccountPriv (AEAD nonce + ciphertext) · the MK-MAC
bucket vflin-blobs per blob a cleartext manifest (VFB1 + JSON): profile id · device id · version vector · coreVersion · plaintext payloadSize · inner content hash — documented in PROFILE_STORAGE_SYNC.md §“manifest (cleartext, integrity-protected)” wrappedDek · sealedRecord (name / fingerprint / proxy) · the payload — AEAD, opaque bytes
audit_log, orgs, shares, subscriptions empty after the flow: the daemon never ships action-log entries (log.append is unreachable from it), no team was created, no plan bought —
password_resets (S4) which account a reset is for, its expiry, whether it was used — and a hash of the mailed token (token_hash), never the token; a row with a NULL account is a decoy an unknown email leaves so reset-init does the same work on both branches the reset token itself (mailed once, 30-min, single-use)
billing_events (S4) the account an event was for, its plan/status, the provider’s order reference, when it arrived — a payment-history read for the cabinet’s invoices —

So an operator with root on the VPS learns: who has an account, how many devices and profiles they have, how large each profile is, which Chromium version last opened it, and when it changed. They learn no passphrase, no profile content, no cookies, no fingerprint, no proxy. Say exactly this to users; do not say “the server stores nothing”.