Dated source reference. Current release status: handoff state. Full original relative paths are in the developer archive.
Cloudflare deployment and operations
The canonical browser editor is https://openhoops.io/rostereditor; the homepage is reserved. Earlier web0.3.0.dev1/source56d, Worker9861efef-0ee1-452f-a01d-c40828f1dad0, adds public email accounts and full workspace recovery while preserving the accepted55t writer payloadcf05198c. Current evidence records exact identities and acceptance boundaries.
Current web0.3.0.dev2/source58c, Worker2bb99237-86cb-4722-891d-4dc8810f5090, adds private roster starters. The earlier56 milestone below retains its account/email evidence. Starter manifest and acceptance records current source and scope. The exact Python payload is unchanged. Versioned starter ZIPs live under starters/v1/{sha256}.zip in the existing private R2 bucket, outside public assets. Preserve all four objects when backing up/migrating the service; source cloudflare/starter-assets.json binds size/hash. Provision and verify these assets before enabling template choices on a new deployment. Never enable public bucket access to serve starters.
Topology
- Worker static assets serve the browser interface and same-origin HTTP API.
RosterProjectSQLite Durable Objects own project permissions, head revisions, idempotent receipts, comments and version history. A per-project queue serializes saves; SQL transactions commit the head and receipt together.- Private R2 objects hold immutable compressed ROS snapshots, metadata and cell patches. Originals remain the first saved revision. Restores append a revision. No automatic pruning or project-deletion endpoint is enabled.
RosterAccountSQLite objects hold verified-email profiles, hashed sessions/recovery keys, invitations and account-scoped workspace organization.AuthLimiterobjects distribute sign-in throttles by hashed IP. Project membership remains authoritative in the project object. New migrationv2-accounts adds these classes without replacing PROJECTS/PROCESSOR namespaces.RosterProcessorruns the shared Python writer in a Cloudflare managed Debian container. It loads only our packaged writer, factual catalog and college schema. Its disk is temporary; R2/SQLite remain authoritative. It installs the exact packaged writer on first use and runs one foreground Python HTTP interpreter on internal port8080. The interpreter is reused across requests/object-heap reconstruction and stops after five minutes of inactivity. The first cold request can take longer. Custom resources are1vCPU/3072MiB/2000MB; the resource-size/version labels replace stale owned stateless containers before use.
The production service has no Dell dependency. The local environment is a QA adapter: Wrangler/SQLite/R2 plus a loopback Python bridge on the Dell. It never belongs in a public deployment.
Authorize the correct account
Use Wrangler device authorization from a trusted existing CLI. The user opens Cloudflare device verification on their phone and enters the current five-minute code. Expired codes cannot be reused. Tokens stay in Wrangler's private configuration; do not paste them into chat or source files.
Required permissions include Workers scripts/routes, account/storage access, Containers, zone read and email_sending:write. Preserve existing Pages access for other projects. A successful Pages-only whoami is insufficient: verify the openhoops.io zone's account ID, zone status, current DNS, Workers, R2 and Containers entitlement through the live account before changing anything. Specify that account explicitly because multiple accounts exist. Do not silently upgrade billing. Pending domain/plan/authorization gates must be reported.
Build and deploy
Build on the Dell using a verified official portable Node and Python. Current lockfile pins Wrangler4.151.1 and Workers types5.20261018.1. The writer has no third-party Python dependency.
cd roster-editor/web
npm ci
python tools/build_runtime.py
node --check cloudflare/worker.js
node --check public/rostereditor/studio.js
npx wrangler types
npx wrangler deploy --dry-run
writer-runtime.zip is generated on the Dell and imported as a private Worker data module. The build strips supplied help prose and excludes vendor programs, game assets and rosters. Keep its hash with the deployed source manifest. Generated types come from the actual Wrangler bindings; do not hand-maintain an Env interface.
After live account inspection, set the verified account_id; create or deliberately reuse the private openhoops-roster-projects R2 bucket. Do not enable public R2 access. Set a cryptographically random CREATE_KEY through Wrangler secret input. It authorizes initial project creation and never belongs in the public bundle. Create a custom-domain binding for openhoops.io only after checking its existing records. Leave artwork and historical preview hosts/packages untouched.
Deploy the top-level production configuration, explicitly selecting it when Wrangler requests an environment. Do not deploy --env local. Verified email accounts create their own workspaces without a setup secret. The setup key is retained for administration/legacy compatibility. Create an empty owner project through POST /rostereditor/api/projects using the setup key, then give its owner link privately to the user. No original roster needs to be seeded publicly. Existing project owners can create further projects with their owner capability.
Validate on the actual Cloudflare service: private upload; Python cold/warm operation; two WebSocket clients; viewer refusal; concurrent edits; repeated command; export/readback; durable recovery; then the exact live browser result. GET /rostereditor/api/status alone does not prove the writer works. Confirm the custom domain separately from a workers.dev deployment. Preserve the exact source, bundle/runtime hashes and acceptance scope.
Local QA
Use wrangler dev --env local --ip 127.0.0.1 --port 8793 on the Dell. Supply private CREATE_KEY and LOCAL_ENGINE_KEY through .dev.vars.local. Start tools/local_engine.py with the matching OPENHOOPS_LOCAL_ENGINE_KEY; it listens only on127.0.0.1:8794. OPENHOOPS_WRITER_RUNTIME can select an independently unpacked copy of the exact production writer payload for acceptance.
Keep the SSH parent job alive while running these Windows processes. On completion stop only the recorded, identity-checked owned process trees. Retain the local persistence directory for restart checks; never erase it as an implicit reset. Forward port8793 over SSH for Paseo browser work. Use native mcp__paseo__browser_* tools; report a screenshot/host limitation explicitly.
Access and recovery
Email accounts and owner/edit/view bearer links coexist. Named membership uses HttpOnly same-origin sessions and CSRF protection; existing links need no account. The token appears only in a URL fragment on entry, then the app removes the fragment. HTTP uses Authorization; WebSocket authentication uses an offered subprotocol. The server stores token hashes, enforces roles and closes revoked connections. Named account email control is verified; display names are user chosen and anonymous labels remain unverified. The browser retains its recent capabilities locally, so use a trusted browser profile. New share-link secrets are displayed once.
Viewers can inspect data and comments. Owners decide whether they may download ROS/CSV. Editors can edit shared data; only owners manage links/settings and restore whole versions. Keep at least one named owner or active owner link. Separate projects have separate high-entropy IDs and grants; there is no public project listing.
Cell commands require original cell values and a row-layout epoch. Different cells may merge. Same-cell conflicts refuse the whole batch. Bulk/structural/script/restore commands require the exact current revision. Command IDs and payload hashes make roster saves safe to retry. Before sending, the browser journals the exact command in IndexedDB. Retryable network/5xx failures preserve it across reloads and fresh tab identities for the same project/grant. Rejected/conflicting commands remain reviewable and downloadable until explicitly discarded. Unfinished cell text is saved separately in browser storage; unsent bulk/share/script form text has no recovery guarantee. Local storage failure refuses sending, and clearing site data removes local drafts. Offline editing is not advertised.
Sequential Undo/Redo persists for the current browser tab identity and applies inverse cell changes only while their saved after-values still match. An intervening teammate edit to that cell blocks it. Structural operations form an explicit browser undo boundary; API structural undo requires that operation still be the current head. Whole-version restore is an owner action that appends a new snapshot and changes the row epoch. Comments retain their original epoch rather than attaching themselves to guessed moved rows.
Current bounds:24MiB HTTP JSON,16MiB companion,50,000 cells per command,120 visible columns,250 rows per grid request,100 active links,60 simultaneous project sockets and10,000 comments. These are finite implementation bounds; maximum-size opaque payloads have not been certified against production memory limits. The accepted large specimen is905teams/13,665players/3,779uniforms. Capacity/health checks do not certify every game mode.
Managed Cloudflare persistent HTTP operation and40 production collaboration cases pass, with12 routing/access cases and live905-team export/reimport. A local harness injects R2 snapshot/patch and SQLite transaction failures against the production project class, real SQLite and the actual Python writer: seven failure/retry/reconstruction checks preserve the saved head and commit once.36 local collaboration cases include history pagination beyond100 revisions. Six simulated processor-lifecycle checks cover exact installation, recreated heap, stale replacement, failed installation, retry and serialization. These are separate from production-provider fault injection.
Processor operations and cost scope
The foreground command waits for .runtime-sha256 to equal the expected payload hash before execing Python with explicit PYTHONPATH. Readiness and request headers also verify the version. Do not run stale code after a failed install. Python's HTTP bind intentionally skips reverse DNS: Cloudflare's internal hostname exceeded an IDNA label bound in the default HTTPServer.server_bind. HTTP requests have explicit content length/version headers; valid bodies are drained before early refusal. The final acceptance uses mode:http; the guarded exact-runtime CLI fallback is infrastructure degradation handling, not the accepted fast path. Diagnostic timing/cache/container/mode headers are available only on authenticated read responses; there is no public debug endpoint.
Keep the full authoritative state in SQLite/R2, never on container disk. Source/runtime/resource-size changes may replace only the owned stateless processor. The accepted processor uses the custom1CPU/3072MiB/2000MB size and300-second inactivity sleep. Under published Container rates, memory/disk total about$0.027504 per running hour before included allowances; actual CPU and Workers/DO/R2 usage are separate. No billing plan was changed. Observe real billing as editors start using it; idle/cold startup remains expected after sleep.
Final samples: cold grid7.99s, warm186ms,20-second-idle479ms;905-team upload9.12s,13,665-player bulk2.46s, export1.34s. No SLA is inferred. Preserve source/payload/deployment identities when investigating regressions. Maximum opaque payload memory, service-wide administrator disaster recovery/retention, pixel inspection and physical cross-browser certification remain unverified. Portable ZIP export protects the selected ROS/companion pair. The separate Full backup format now protects workspace grants/comments/history/receipts. Server originals/versions remain immutable without pruning.
Email sign-in operations
The sender is signin@accounts.openhoops.io, isolated from the apex mailbox domain. Existing Email Routing/apex MX settings were not changed. A fresh user-approved Wrangler device grant added Email Sending write while preserving Workers/Pages/Containers scopes. offline_access is automatically added by Wrangler; explicitly requesting it can cause an invalid-scope error. Never retain a five-minute device code as reusable credentials.
Onboarding used wrangler email sending enable accounts.openhoops.io --zone-id <verified-zone-id>; Cloudflare created the dedicated bounce-domain MX/SPF, DKIM and DMARC records. Public DNS confirmed all records. The production EMAIL binding restricts sender addresses to signin@accounts.openhoops.io and intentionally allows arbitrary recipient inboxes for self-service sign-in/invitations. No personal email account or mailbox password is needed. Do not enable routing or alter apex MX to reproduce sending setup. See Cloudflare sending setup and native Workers send API.
Codes have ten-minute expiry, single-use verification and bounded attempts/resends. Durable sessions store only hashes; production cookies are Secure/HttpOnly/SameSite=Lax. Account recovery rotates its key and revokes previous sessions. The initial key and replacements are shown once for private download. Retain no codes/cookies/recovery keys in logs or evidence. Email delivery failure is reported as unavailable rather than silently creating an account. Invitation notifications use a durable outbox; a retry can complete a delivery whose membership/inbox record already committed.
LOCAL_AUTH_MAILBOX=enabled is strictly a local QA simulator and additionally requires LOCAL_ENGINE_ORIGIN. It stores synthetic messages in private local R2; it is absent from the production environment and has no public mailbox endpoint. Production delivery verification uses controlled QA inboxes, never fake production recipients or personal mailboxes. Mail.tm provided those temporary QA inboxes; it is not an application dependency or the production sender. Monitor real Email Sending/Workers/DO/R2 billing; provider quota fields were null and do not prove a free/unlimited allowance. No billing plan was changed.
Complete workspace recovery operations
Owners can download Full backup in the editor or dashboard and restore it from My workspaces. The .ohworkspace.gz file includes all referenced original/version objects, immutable receipts, comments and permissions. It does not contain account sessions/recovery keys or plaintext share-link tokens. A frozen SQLite row copy and immutable R2 keys make concurrent edits safe while streaming a backup. Checksums protect each object and the full sequence.
Recovery creates a separate empty workspace, streams bounded resumable batches and publishes its head/history only after final validation. The default private copy retains permission records without activating them. Explicitly selecting access restoration reactivates saved memberships/link hashes; recovered URLs use the new ID. Retain backup files privately. If interrupted, choose Resume recovery and reselect the same file, keeping the original access option. Lost final responses are recognized after reload. Nothing replaces the source workspace. Incomplete recovery rows/objects are durable but no automated staging-retention policy is enabled; operators should observe storage rather than delete history implicitly. This is complete workspace recovery, not a global service/account disaster-recovery certification.
See the service contract for finite account/upload/backup limits and access rules. Existing immutable historical ZIPs, source snapshots and original user owner access remain protected.