- Shell 92%
- Docker 8%
cavu-pds #
The cavu account server: vlpds at https://account.cavu.social, hosting *.cavu.social handles, deployed as the Fly app cavu-pds in the cavu org.
It's the OAuth sign-in server for cavu.games and cavu.page accounts, and it stores large blobs (up to 1 GiB) so cavu.games can distribute games over atproto.
How sign-in works #
Each cavu site has its own sign-in page with one field (handle or email) and a "Sign in with Google" button:
- A handle goes through standard atproto OAuth to whichever PDS hosts it. A bare name with no dot (
chris) meanschris.cavu.social. - An email starts OAuth at
https://account.cavu.socialwith the email aslogin_hint. Only cavu.social accounts can sign in by email, since atproto can't find a PDS from an email address. - Google starts OAuth at
https://account.cavu.socialand addsprovider=googleto the authorize URL, which skips the sign-in page and goes straight to Google. A new Google identity gets the sign-up form, with a random handle filled in that the person can change.
Google is a vlpds sign-in provider (upstream's OpenID Connect support, see upstream/docs/operations/sign-in-providers.md), configured at runtime, not in fly.toml.
Build #
vlpds is built from source. upstream is a submodule pinned to a main commit (there are no release tags), and patches/*.patch is a git am series on top.
scripts/prepare.sh # .build = upstream + patches, one commit each
# edit and commit in .build, then:
scripts/save-patches.sh # export .build's commits back to patches/
cd .build && cargo test --test all
To bump vlpds, check out a newer commit in upstream, run scripts/prepare.sh, and fix any patch that no longer applies. If git am can't 3-way merge (it needs the blobs the patch was made against), apply the series on the old base in .build and git rebase it onto the new one instead.
Each patch is written so it could go upstream: behavior behind a flag or config, defaults matching upstream, and tests in tests/all/.
Patches #
-
0001 first-party clients.
VLPDS_FIRST_PARTY_CLIENTSlists cavu.games' and cavu.page's OAuthclient_ids. Their tokens are trusted like a password session for account security, with a sign-in in the lastVLPDS_FIRST_PARTY_REAUTH_SECSneeded to change how an account signs in. From poasters.club. -
0002 passkey related origins.
VLPDS_PASSKEY_RELATED_ORIGINS, served at/.well-known/webauthn. From poasters.club; unset here for now. -
0003 bare names and email hints. A sign-in identifier with no dot is a handle under the primary domain (
chrisischris.cavu.social), on the OAuth page, inlogin_hintand in createSession.login_hintmay be an account's email. No flag: bare names never resolved before. -
0004 suggested sign-up handles.
VLPDS_SUGGEST_SIGNUP_HANDLEfills in sign-up forms (the OAuth page's and a sign-in provider's) with a random available handle likebrave-otter-42, which the person can keep or change before the account is created. -
0005
provider=on the authorize URL./oauth/authorize?…&provider=googleskips the sign-in page and goes straight to that sign-in provider, for sites with their own "Sign in with Google" button. -
0006 Profile page.
/account/profilein the account app editsapp.bsky.actor.profile(display name, bio, photo, banner) with standard XRPC calls. UI only. -
0007 controlled accounts.
VLPDS_CONTROLLED_ACCOUNTS: accounts with controller DIDs instead of a password and an email (game characters, NPCs). A controller creates them (vlpds.account.createControlledAccount, up toVLPDS_CONTROLLED_ACCOUNTS_PER_CONTROLLER, overridable per controller in the console) and makes a character password: an app password withcharacters(and optional collectionscopes) that signs in as those characters with a plaincreateSession(characterHandle, password), never as the controller and never for account or identity changes. The operator converts existing accounts withvlpds.admin.setAccountControllers(removePassworddrops a stopgap password). PLC and deletion codes go to the controller's email. Account app: a Characters page. Blob usage is not pooled with the controller's quota. Details:docs/operations/controlled-accounts.mdin the patched tree. -
0008 confirm sign-up.
VLPDS_CONFIRM_SIGNUPasks "Create a new account?" before an OAuth sign-in leads to one, naming the app (itsclient_name): a sign-in provider identity no account uses (Create a new account / Use an existing account, back to the sign-in page for the same request), or an email or handle here with no account, aslogin_hintor typed on the sign-in page (Create a new account, prefilled / Try a different email or handle).prompt=createskips it. It tells the visitor an email has no account here, which createAccount already reveals; off by default upstream. -
0009 sign-up methods.
VLPDS_SIGNUP_METHODSmakes the OAuth sign-up page offer, in order, Continue with Google (each sign-in provider whosesignupis open or invite), Create with a passkey (handle and email, no password) and Or use a password (the old form, folded away only where a passkey can be made, so it's the whole page without JavaScript or WebAuthn). Continue with … from this page skips 0008's question for a new identity and just signs in a linked one. The passkey runs every createAccount check first, then mints the DID ahead (a discoverable passkey's user handle is the DID; key held as a reserved key, PLC genesis op kept unsent on the request) and asks the browser for the passkey; the account is created only after the passkey verifies, with it as the only way in (no password, like a provider signup). Cancel, Back or a failed ceremony creates nothing. Off by default upstream; the account app's/account/signupis unchanged. -
0010
login_hinton the authorize URL./oauth/authorize?…&login_hint=sets the hint when the pushed request didn't, checked as PAR's is (0003) and kept on the request. @atproto/oauth-client-browser overwriteslogin_hintin the PAR, so cavu.page can append it to the authorize URL instead of patching the PAR body. -
0011–0012 theming (upstream-first). Operator theming from
upstream-patches/theming/(the copy for submitting upstream), carried here until it lands: a runtime theme (vlpds.admin.putTheme,vlpds admin theme, the console's System > Theme) with a name, logo, favicon, light/dark colors, a font from a fixed list and home/terms/privacy/support links, rendered on every OAuth page (including 0008's "Create a new account?" and 0009's sign-up methods and "Save your passkey", the theme's stylesheet hash joining the CSP next to the sign-up script's), the account app (0006's Profile and 0007's Characters included), /migrate, the landing page and account email. 0011 is the server, docs and tests; 0012 the UI. Our theme istheme/cavu-theme.jsonwiththeme/cavu-logo.pngandtheme/cavu-favicon.png;VLPDS_ADMIN_TOKEN=… theme/apply.sh http://localhost:2583(throughfly proxy) puts it. Details:docs/operations/theming.mdin the patched tree. -
0013 confirm the email before the account.
VLPDS_REQUIRE_EMAIL_CONFIRMATIONcreates an account only once its email is confirmed. A password or passkey sign-up on the OAuth page (and a Google sign-up whose email Google didn't vouch for, or that the person changed) becomes a pending sign-up after every createAccount check: its DID minted ahead with a reserved key and an unsent PLC genesis op (as 0009's passkey does), its handle and email held, the password hashed; no account, repo, firehose event or PLC op yet. "Check your email" takes the mailed 6-digit code (or the mail's link, in any browser, whose page has one Confirm button) and then the account is created, email confirmed, and the OAuth flow goes on to consent. Resend (a minute apart), Use a different email (keeps the handle), five wrong codes cancel it, 24 hours lapse it (handle, email and key released); a refused mail (spent mail budget) keeps it with "We couldn't send the email right now". Plaincom.atproto.server.createAccountsign-ups are refused (migrations with service auth, the admin token,vlpds.admin.createAccountandcreateControlledAccountstill work); the account app's/account/signupusesvlpds.account.startSignup/confirmSignup. Bucketssignup-pending-ip(5/h) andsignup-mail-address(5/day). Details:docs/operations/email-and-moderation.md#confirming-the-email-before-the-accountin the patched tree.
0003–0013 are written for upstream: each is one feature with its own tests, off by default where behavior changes. Sign-in with Google itself is upstream's sign-in providers, so poasters.club's linked sign-ins patches aren't carried here.
prompt=create (open the sign-up form) is already upstream.
Deploy #
scripts/deploy.sh
fly logs --app cavu-pds
fly proxy 2583 --app cavu-pds # console: http://localhost:2583/admin (VLPDS_ADMIN_TOKEN)
Caddy listens on 8080 behind Fly's edge and returns 404 for the operator paths (/admin, /xrpc/vlpds.admin.*, /metrics, /internal/*). vlpds listens on [::]:2583, reachable only over the private network.
Spaces (VLPDS_SPACES, AT Protocol permissioned data) is on: cavu.page's reading groups and other cavu features need it. It's an alpha that changes weekly, so bump upstream often, and once space data exists never roll back to a vlpds build from before Spaces. Checks: vlpds admin check-space <did> <space> and the vlpds_space_* metrics (upstream/docs/spaces/operating.md).
Tiny profile: one node, one shard, a 60 s lease, on a shared-cpu-2x with 2 GB. Never run two machines. Everything durable is in the Tigris bucket cavu-pds under the prefix pds.
Storage limits #
Sign-up is open, so the per-account storage quota (VLPDS_BLOB_QUOTA_GB, 2 GB) is what stops it from being free file hosting. Raise it per publisher in the console. Single blobs can be up to 1 GiB (VLPDS_MAX_BLOB_MB).