# Online records and deployment

Movement remains local at the same fixed Source interval. Accounts and ranked
records are optional: losing the service does not stop surfing, practice or local PBs.

## Architecture

- Vercel CDN serves the Vite build, visuals and immutable gzip collision packs.
  Compression changes transfer size, never geometry coordinates or physics.
- Supabase Auth handles Google/X OAuth with PKCE. Only the project URL and
  publishable key reach the browser. The Node API authorizes private requests
  using Auth `getUser`, never by trusting a decoded client token.
- Vercel Node functions run in Frankfurt beside Supabase. `api/surf.ts` handles
  profiles, leaderboards, attempt tickets and submissions. Private replay uploads
  go directly to Storage, avoiding function request-body limits.
- The durable `surf-verify` queue invokes `api/verify.ts`, which re-simulates
  commands against pinned server geometry and movement settings.
- Postgres transactions enforce quotas, worker leases, atomic PB replacement,
  duplicate prevention and deletion. Surf tables have RLS enabled with no browser
  grants or policies. Only the trusted service role accesses them. Supabase's
  informational [RLS-without-policy notice](https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy)
  is intentional here; adding a permissive client policy would weaken this design.

The server derives time and ordered splits from fractional simulation events,
stored as integer microseconds. It requires the canonical normal spawn, no inherited
momentum, exact configuration, bounded commands, every checkpoint and a finish
without resetting. Browser-supplied elapsed times are never accepted.

Practice, restores, demonstrations, pauses, focus interruptions and changed rules
are excluded in the UI. Server validation independently checks the complete run.
This is **server-validated movement**, not proof of human input: valid command
sequences can be synthesized. Exact duplicate digests are rejected, but this is
not comprehensive anti-cheat. Keep the public leaderboard labelled beta.

## Version identity

`scripts/build-online.ts` hashes collision JSON bytes and movement/session/timer
sources, normalizing source line endings for consistent Windows/Linux identities.
Board IDs include map version, both SHA-256 hashes, all movement settings, protocol
and canonical-spawn policy. The browser refuses ranking against a different build.
Rendering/audio changes do not create new boards. Verification-rule changes need
deliberate protocol review even when the movement source remains unchanged.

The small generated `src/online/catalog.generated.json` is tracked. The server
packs in `.online-build/` and public packs in `public/packed/` are generated by
`npm run prepare:online`, automatically run before development, tests and builds.
New movement/map identities need new board rows, not changes to an old board.

## Configuration and deployment

The existing Vercel project is `surfd`. Node 22 is pinned by `package.json`.
`vercel.json` specifies Frankfurt, queue delivery, daily maintenance and server
map inclusion. GitHub checks run tests, build and the production dependency audit.

Configure these server variables for each deployment environment:

| Variable | Purpose |
| --- | --- |
| `SUPABASE_URL` | Project HTTPS URL |
| `SUPABASE_PUBLISHABLE_KEY` | Public `sb_publishable_…` key |
| `SUPABASE_SECRET_KEY` | Private `sb_secret_…` key, marked sensitive |
| `SURF_AUTH_PROVIDERS` | Actually enabled providers, e.g. `google,x` |
| `CRON_SECRET` | Random private maintenance secret |

Never prefix private credentials with `VITE_`. `.env.local`, `.vercel/` and test
outputs are ignored. The config endpoint returns only explicitly allowed public
values. Missing cloud settings produce an honest local-only state.

Apply the migrations under `supabase/migrations/` in order. Seed the generated
catalog into `surf_boards`: `boardId` becomes `id`, `id` becomes `map_id`, `version`
becomes `map_version`; include both hashes and config, and set `active=true`.
Deactivate retired boards instead of silently changing their rules.

Google/X client credentials belong in Supabase Auth. Provider callback:
`https://wzyozuhomwufeuqakuai.supabase.co/auth/v1/callback`. Supabase's separate Site
URL/redirect allowlist must allow the actual app origin (and specific previews
used to test login). The browser returns to `/`. Google testing mode requires
listed test users; public login needs the appropriate audience setting.

Deploy and validate a preview before promoting. Deployment protection must allow
the intended audience before describing the beta as public. Preview and production
currently share Supabase; separate projects are preferable if strict isolation is
needed. OAuth provider configuration is distinct from local browser fixture tests.

## Resource bounds and recovery

Restart tickets expire after 45 minutes and grant no Storage access. Only finishing
requests signed upload permission. A database reservation covers outstanding tokens
and existing objects. Capacity is capped at **32 MiB per account and 700 MiB globally**,
below the free Storage allowance. Each signed upload reserves its full 1 MiB limit
for **125 minutes**, even if its current object is smaller. With no older stored
replays, this permits up to **32 upload authorizations per 125 minutes**; retained
replay bodies reduce the available allowance. Reauthorizing the same attempt renews
its existing reservation. Restarts consume no storage allowance. Accounting uses
the larger of the live reservation and actual object size for each path, in one
database snapshot; account deletion cannot release still-live token capacity.

Uploads are capped at 1 MiB gzip, 8 MiB after expansion and 40,000 commands (about
ten minutes including preparation). Per-account request quotas allow 1,200 attempt
tickets and 120 submissions per hour. These are beta bounds, not unlimited hosting.

Signed uploads cannot overwrite existing objects. A lost upload response recovers
through idempotent submission; reloading between upload and submission also retries
submission. Database leases and transactional completion tolerate duplicate queue
delivery. Operational errors are retried rather than labelled invalid movement.

Authenticated `/api/maintenance` recovers a bounded batch of stranded submissions,
removes abandoned/rejected replay bodies after signed-token expiry, and removes
non-best replay bodies after seven days. Best bodies remain; verified numeric
history remains after body expiry. Daily Vercel Hobby cron runs in production only.
Previews require manual authenticated maintenance. Daily recovery can take up to
a day; queue delivery and active submission polling normally recover sooner.

Maintenance checks `CRON_SECRET` before any work, with bounded batches and a work
deadline below its function limit. Account deletion hides records, removes replay
bodies, revokes sessions and deletes the Auth user. Outstanding token reservations
outlive deletion until expiry to prevent a storage quota bypass.

Watch database size, Storage use, egress, verifier errors and queue age. Upload
allowances do not cap every infrastructure bill or prevent every denial of service.
When capacity is exhausted, ranked uploads fail gracefully and local play continues.

## Validation

`npm test` includes movement regressions, strict replay validation, gzip loading,
API authorization, retry/crash recovery, actual PostgreSQL through PGlite, quotas,
atomic PBs, rank ties, account deletion, maintenance guards and client lifecycle.
`npx tsx tests/online-client.browser.ts` exercises browser flows with explicit
service fixtures; these do not establish live OAuth or queue operation.

`fixtures/online-canonical-replay.json.gz` completes Utopia from its unchanged
production spawn. The first 664 ticks walk across the start deck using ordinary
commands before joining the established route. The production verifier reproduces
**53.349069 seconds** and all three checkpoints. Its 4,531 total commands take
about 67.965 seconds including preparation; cloud testing must allow that wall time
between issuing the ticket and submitting. Automated QA accounts/records must be
removed from public standings after testing.

## Privacy and limitations

Supabase Auth stores provider account identity. Public records expose username,
account UUID, time, splits, date and available verified command replays, not email
addresses or OAuth credentials. No analytics or advertising SDK is added.

Original CSS map assets retain their authors' rights; hosting does not grant a new
licence. Existing uncertainties remain in `THIRD_PARTY_NOTICES.md`. Exact universal
CSS/KSF parity, human-only anti-cheat and unlimited free operation are not claimed.

## Verified deployment evidence (3 October 2026)

The full 232-test suite and production dependency audit passed (zero vulnerabilities).
Replay/database checks also passed on Node 22. Native Node ESM loading is tested
without TypeScript hooks: `scripts/build-server.ts` bundles internal server and
simulation imports into explicit `.mjs` entrypoints before Vercel packages them.
This fixes the startup failure found in the first real preview; local TypeScript
tests alone did not detect that packaging difference.

`fixtures/online-cloud-validation.json` records a successful real preview run:
Auth account creation/sign-in, profile update, ordinary attempt/upload APIs, private
immutable Storage upload, queue verification, exact time/splits, atomic repeated
submission, public leaderboard, intact replay download and account deletion.
A subsequent direct database check confirmed zero remaining QA users, records or
replay objects. Both temporary QA deployments and the QA secret were removed.
The helper is preserved only as an opt-in test fixture under `tests/support/`;
no test bootstrap endpoint is present in the normal deployed API.

The deployed browser loaded Boreas and its leaderboard with no console errors.
Google login reached Google's real sign-in page. Completing a human Google/X login
and verifying the final return remains a user check; this is separate from the
successful real Auth-token/backend integration test.

## UI review branch — 3 October 2026

The UI pass retains the Source/VGUI panel style with a shared visual system for
the main menu, Settings, map picker, accounts, leaderboards and finish screen.
The visual pass itself leaves movement and ranked-board identities unchanged (the
subsequent capped-start rule change is documented below). Google uses its official
current sign-in mark; provider actions remain optional. The later guest-claim
flow below supersedes this pass's initial next-run-only sign-in prompt.

`GET /api/surf?action=run&runId=UUID` exposes the public name, map, server time and
splits of a visible verified replay on a current active board. It excludes private
upload paths and account data. `/?map=boreas&run=UUID` opens a run card with replay
playback. Sharing opens an editable X draft; it never posts automatically. Local,
practice and watched-route shares explicitly say what they are and link to the
map, rather than presenting a verified replay link. All displayed times round to
milliseconds consistently, including across minute boundaries.

For local review, run `npm run dev -- --port 4190` and open `/ui-review.html` for
sample finish, sign-in, leaderboard and sharing states. This gallery uses an
isolated fixture client; no accounts or records are created. It is not a production
build entry. The real playable preview remains `/`. Production deployment was
held for review; the user approved this release on 3 October 2026.

Validation for this pass: all 243 automated tests passed, as did 14 isolated
browser integration contexts and 22 UI checks. `npm run test:online:browser`
exercises auth handoff, submission, replay, cancellation and sharing against
fixture APIs; it does not complete a real Google/X login or publish a record.
With the dev server on port 4190, `npm run test:ui` checks the playable menu and
the isolated gallery, including keyboard focus and 390×740 / 800×480 layouts.
Its report and seven screenshots are written to `test-results/`. Override
`UI_BASE_URL` to use a different local server.

The production build's normal Boreas command replay completed in 00:39.495,
with checkpoint splits 00:16.594 and 00:32.681. Browser inspection found no
console errors. The embedded review browser cannot capture the mouse, so use
Chrome or Edge for manual surfing. New online UI flows were checked with fixture
APIs; their deployed integration still needs a preview check before promotion.
The review gallery is excluded from the production build.

## Capped ranked starts — 3 October 2026

The normal client profile, build catalog and trusted verifier now use
`RANKED_CONFIG` (`css-surf-capped-1`): auto bunnyhop enabled, unrestricted start
speed disabled. The existing Boreas-derived end-tick XY start cap is enabled;
see [the movement reference](REFERENCE.md) for its exact semantics and evidence
limits. Old `SURF_CONFIG` open-start witnesses remain replayable and unchanged.
They are rejected by the new ranked verifier. New local record keys and generated
board IDs keep the categories separate; old records must never be relabelled.

Preferences v3 imports v2 controls, sensitivity, view, audio and map preferences,
but resets the old default unrestricted-start flag to false. A fresh explicit
unranked opt-in persists. Settings and leaderboard panels explain the capped rule.
Guest messaging distinguishes eligible prepared runs from local-only finishes;
the guest-claim flow below permits authentication after an eligible finish.

Before deployment, seed the three new catalog board rows as described above and
retire the old boards without deleting their records. The approved release has
seeded the new boards without relabelling older records. Check the new boards
and a real submission on a preview before promoting the UI and rule changes.
Drain old-profile pending verification jobs before cutover; the new verifier
cannot finish those jobs under different rules. Replays and published times keep
their original identities. The unranked open-start option also preserves access
to existing local PBs by using the legacy configuration version.
The canonical Utopia fixture has a newly verified envelope/report for the capped
profile; its normal command sequence is unchanged. The prior cloud validation
file remains historical evidence for the previous deployment, not a cloud test
of these new board IDs. `tsx scripts/validate-classics.ts --ranked` regenerates
`fixtures/ranked-playability-results.json` for all three capped command routes
and exact 30/60/144/240 FPS schedule comparisons.

Local validation passed: 263 automated tests, 14 isolated online browser contexts,
23 UI checks, and the production build. All three capped routes complete, with
exact per-tick results at 30/60/144/240 FPS. No real account or database record was
created during this follow-up.

## Finish first, sign in to save — local review

The normal default-rules guest flow now requests a signed guest ticket before the
first movement command. A slow or unavailable service never blocks play: a run
started before its ticket is ready remains local, with honest finish messaging.
Finishing an eligible run stores its exact command replay and ticket in IndexedDB.
Google/X buttons commit an explicit save intent before leaving for OAuth. On return,
the finish screen and splits are restored, the ticket is claimed by the signed-in
account, and the existing immutable upload and server verification flow runs.
Only a verified server response upgrades sharing to a public replay link.

`POST guest-attempt` allocates no Auth account, database row or Storage object.
Its 24-hour HMAC proof binds a random attempt UUID, board identity and server issue
time. The signing key is domain-separated from `SUPABASE_SECRET_KEY`; no new secret
is needed. Treat the proof as a private bearer capability: it never belongs in a
URL, share link or log. Rotating the server secret invalidates unclaimed proofs.
`POST guest-claim` requires a real Auth user and matching `expectedOwnerId` before
claiming. The atomic database ledger binds that nonce to exactly one account,
including after account deletion, until the proof expires. Same-owner retries
return the same attempt; the upload window is not extended. A new claim grants
the ordinary 45-minute window. Registration time, separately from original issue
time, enforces the shared 1,200-attempt/hour quota.

The browser keeps one completed guest replay, bounded by the existing 8 MiB /
40,000-command limits. A newer finish can replace an unclaimed draft only before
the player has requested saving it. Authorized, owned and claimed drafts are
protected until saved or explicitly dismissed. IndexedDB transactions bind owner,
claim progress and conditional deletion to the attempt UUID so stale tabs cannot
overwrite another run. Unclaimed drafts expire with their 24-hour ticket; claimed
drafts remain recoverable for seven days, including when only verification remains
after the upload window closes. Expired entries are removed on the next access.
Storage failure stops OAuth navigation and offers a retry instead of silently
losing the replay. Cancelling login keeps the run available in Account.

Practice, open starts, changed movement rules, interrupted runs, expired/unprepared
tickets and watched replays do not become ranked through login. The browser's time
and splits are presentation only; server simulation computes the accepted result.
This is not evidence of human-only input. The original server verification and
anti-automation limitations still apply.

Release prerequisites: apply `20261003155331_signed_guest_attempt_claims.sql`, seed
the new capped-start boards, and validate before promotion. The schema and board
setup were applied after the user's release approval on 3 October 2026. The
browser integration suites use explicitly routed fixture services, not real Google
or X identities, and the SQL tests apply all migrations to local PGlite.

Local validation passed: 283 automated tests, 14 existing online browser contexts,
15 new guest-save browser contexts, 11 real IndexedDB scenarios, two actual-game
restored-finish scenarios, 26 UI checks, TypeScript and the production build.
The guest flow exercises the real Supabase PKCE client against intercepted fixture
Auth responses, asserts the uploaded gzip replay matches the retained commands,
and tests account changes, login cancellation, duplicate returns, lost responses,
expired upload windows, unavailable storage, and both protected-slot and retry
recovery. This does not claim a completed real Google/X account login on the new
deployment. `npm run test:guest:browser` runs the guest suites; its actual-game
checks need the dev server at `UI_BASE_URL` (default port 4190). Test reports and
screenshots are written under ignored `test-results/`.

A domain change does not migrate local site data: OAuth must return to the same
origin that stored the replay. Keep the existing origin allowed during a domain
transition and avoid redirecting an in-progress old-origin callback to the new one.

## Custom domain — 3 October 2026

`https://surfd.net/` is attached to the existing Vercel production project;
`www.surfd.net` redirects to it with HTTP 308, preserving paths and queries.
The original `surfd-five.vercel.app` origin remains available. HTTPS, all three
collision packs, the scripts, legal pages and records API were checked successfully.
The production deployment was not changed by attaching the domain. The UI,
capped-start and guest-save release was subsequently approved separately.

The user confirmed saving Supabase Site URL `https://surfd.net` and adding
`https://surfd.net/` to the redirect allowlist while preserving existing entries.
The agent could not independently read this setting: the connected database tools
do not manage Auth configuration and the browser dashboard was signed out.
Google and X still use the existing Supabase callback URL above. Public provider
website/privacy/terms links can use the new domain. Online accounts and times use
the same backend; browser-local PBs, settings and login sessions do not move across
origins automatically.

## Approved release checks — 3 October 2026

The guest-claim migration is applied, with RLS enabled and no anonymous or ordinary
authenticated table/RPC privileges. Only the trusted service can consume claims.
All three new board identities were inserted alongside the original boards; the
old boards are retired at cutover without deleting the existing historical record.
There were no pending/verifying jobs at the pre-release check.

Security advisors report the intentional server-only RLS configuration described
above, plus an existing [leaked-password protection advisory](https://supabase.com/docs/guides/auth/password-security#password-strength-and-leaked-password-protection).
The game's sign-in UI uses Google/X, not password registration. No authentication
settings were weakened for this release.

Creating a temporary real-cloud QA account and preview guard secret was rejected
by automatic approval review as outside the deployment request. Neither was
created, and no temporary helper is included in the release. Validation instead
uses the completed local/browser suites, live public endpoints, schema/permission
checks, and production runtime/browser inspection. The optional `cloud-smoke.ts
--guest` fixture is available for a separately authorized cloud-account test; this
release does not claim that new authenticated end-to-end cloud test passed.
