Repository navigation
workspace v2 operations
English | 简体中文
This runbook covers deployment, first-write migration, verification, and rollback for the Workspace Schema V2 coordinator.
-
subman.jsonis the authoritative Workspace V2 document. - One
WorkspaceCoordinatorDurable Object is addressed bygist:<gist-id>. - Every browser and Server API mutation that can change
subman.jsongoes through that coordinator. The public API contract is unchanged; browser mutations remain an internal protocol. - The browser has one cloud binding, with no Workspace list or switching. Both rebind and queue-repair transactions reject replacing an active binding with another Gist, including competing browser tabs and legacy recovery paths.
- Startup loads the latest cloud document before enabling business pages. Login and token replacement retain the existing Gist identity; a failed lookup must never fall through to another Gist or create a new one. An ambiguous initial discovery requires the original Gist ID.
- Browser cache, delivery journal, retry metadata, leases, and historical queue
evidence share the
subman-workspaceIndexedDB v1 transaction boundary throughWorkspacePersistence. This is technical recovery state, not a local business mode or a second source of truth. The database schema is unchanged. - Every valid business action requires authentication, a freshly loaded automatic binding, and no unresolved active delivery. It journals one mutation, sends it immediately, and awaits cloud confirmation. Pending, manual, paused, and disconnected states reject new business changes.
- Settings offers cloud reload and recovery, not local push, merge, force overwrite, bind-only, manual synchronization, or business JSON backup/import. A new bootstrap starts from empty cloud data, never an old browser cache.
-
subman.v1.backup.jsonandsubman.bootstrap.jsonare reserved recovery files. Generated outputs cannot replace or delete them. - The coordinator accepts node, subscription, aggregate, publish-target, and
client-export upserts/deletes; aggregate/client publication;
output.delete;workspace.bootstrap.cleanup; explicitworkspace.migrate; andworkspace.reconcile. No browser page or Server API route may PATCHsubman.jsondirectly.
The checked-in Wrangler configuration binds WORKSPACE_COORDINATOR and creates
the SQLite-backed class with migration tag v1. Migration tags are immutable
deployment history; do not rename or remove an applied tag.
Historical manual or conflict-paused pending queues are retained without automatic delivery. Use the confirmed Reload from Cloud recovery flow; upgrading must not publish a previous browser snapshot.
Workspace actions expose a submitted handle and an authoritative completion
result. submitted means only that synchronous validation accepted the request
and scheduled its task. It does not prove an IndexedDB transaction, queue entry,
or remote write. UI callers must await completion before clearing drafts,
closing editors or reporting success. Only cloud confirmation finalizes a draft;
a deferred result may end the current loading indicator while retaining the
draft and the original pending mutation.
| Completion status | Durable evidence | UI meaning |
|---|---|---|
local-durable |
Historical compatibility result for a locally committed snapshot | Not a cloud save; cloud-only business actions do not produce this result. Retain the draft. |
local-durable-queued |
Snapshot and original mutation committed in IndexedDB | Pending cloud confirmation; retain the draft and journal. |
peer-owned |
Mutation remains in the persistent queue while another valid lease owns delivery | Saved locally; another tab is synchronizing. This is nonterminal. |
retry-scheduled |
Mutation and retry attempt, time, and stable error code are persisted | Saved locally; delivery is deferred or will retry. This is nonterminal. |
remote-committed |
Coordinator result and local binding, baseline, snapshot, and queue settlement agree | Saved to Workspace or published. This is the only publication-success result. |
conflict-or-blocked |
Local mutation remains durable with its exact disposition and stable code | Saved locally, but synchronization needs review or repair. |
rejected-before-durable-commit |
No reliable local transaction was established | Not saved. Retain drafts and show one final error. |
commit-acknowledgement-uncertain |
The core transaction may have committed, but durable evidence could not be reread | Never claim not saved. Retain the draft and reload before retrying to avoid duplication. |
The shared presenter exhaustively maps these results. Stores do not emit
operation toasts, and pages must not infer business state from thrown message
text. Only remote-committed reports success and finalizes the draft. A durable
queued, peer-owned, or retry-scheduled publication remains pending; preserve its
identity and block additional business edits until settlement or explicit repair.
Before every browser business action, require a ready authenticated cloud session, read the current IndexedDB record, reject pending/blocked delivery, and apply the intent to the latest validated cache of the cloud snapshot. A transactional binding advance from a peer raises a controlled concurrent-update result and permits one bounded replay with the same mutation ID and timestamp. It is not storage corruption and does not trigger quarantine. BroadcastChannel and Web Locks remain optional hints; the persisted record, lease, fencing token, revision, and mutation sequence are the correctness boundaries.
After the authoritative transaction commits, cache refresh, lease release, and broadcast are secondary acknowledgement or cleanup work. Failures there cannot downgrade the operation to rejected. The runtime rereads IndexedDB to prove the commit; if proof remains unavailable, it returns acknowledgement uncertainty and must not generate or submit a replacement mutation.
Run the complete local gate:
bun test
bun run test:sing-box
bun run check
bun run lint
bun run build
bun run test:cf
bun run test:e2e
bun run deploy:checkUse Bun 1.3.14 and bun install --frozen-lockfile. The sing-box gate requires
Docker; E2E needs installed Chromium. check includes generated Worker types.
Start the local Cloudflare runtime, inspect the health payload, and verify the unauthenticated mutation boundary:
bun run dev:cf -- --ip :: --port 8787
curl -fsS "http://127.0.0.1:8787/api/health" | jq .
test "$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
"http://127.0.0.1:8787/api/workspaces/smoke/mutations")" = "401"Wrangler local development uses local simulated bindings and Durable Object
storage by default. Without local secrets, health returns HTTP 200 with
ok: false; that is expected locally but is not production readiness. The
bun run test:cf gate exercises an authenticated Worker mutation through the
real Durable Object and SQLite journal with mocked GitHub I/O. Do not use
production GitHub credentials for disposable local migration fixtures.
The checked-in GitHub Actions workflow runs the same test, check, lint, build, Cloudflare integration, and browser-test gates with read-only repository permission. It receives no deployment credentials and never deploys or touches a real Gist.
- Confirm
GITHUB_TOKENandSUBMAN_API_TOKENare configured as Worker secrets. Never place either value inwrangler.toml. - Export the current
subman.jsonfrom each controlled migration Workspace. - Run the pre-deployment gate, including the Wrangler dry-run.
- Deploy the Worker and Durable Object migration together with
bun run deploy. - Require the deployed health payload to report both secrets:
curl -fsS "$SUBMAN_ORIGIN/api/health" | jq -e '.ok == true and .config.githubToken == true and .config.submanApiToken == true'. - Use one controlled Workspace for the first browser or Server API mutation.
- Inspect the Gist and confirm the migration evidence below before widening use.
Deployment or production Gist mutation requires explicit operator approval.
Commit c89a60b is the tested V1-behavior compatibility artifact: it retains
the v1 Durable Object migration, binding, and class export while browser and
Server API writes still use the verified V1 transaction path.
Existing V1 Gists are not rewritten during discovery. On the first successful coordinator mutation, the Durable Object performs one verified Gist PATCH that:
- Copies the exact original
subman.jsonbytes tosubman.v1.backup.json. - Converts all business collections to the V2 document.
- Applies the requested mutation.
- Writes the new
subman.jsonand any publication output together.
Browser connection detects a recognized V1 document and presents an explicit
migration preview before binding or changing the cached snapshot. The user can
cancel, recheck, or choose Migrate and load Workspace. Initial setup may check
an explicit original Gist ID. The separate legacy check requires that ID and is
available only while unbound; it reads without creating a Gist. Connected devices
always retain the same Gist and do not scan or switch to other candidates.
Description, filename, document validity, and identity checks still apply. There
is no local backup download or business JSON import in this flow. Preserve any
historical local-only evidence for separate operator recovery instead of
silently sending it or replacing the selected cloud data.
The internal browser workspace.migrate mutation carries only sourceSha256,
the SHA-256 hash of the exact previewed V1 bytes. It changes no business entities
or publication metadata and sends no output-file changes. The Gist ID, output
filenames, output contents, publication timestamps, and recorded URLs remain
unchanged. Output files without saved publish targets are retained too.
The coordinator checks the source hash again before journaling or writing.
A changed preview or a different current schema returns revision_conflict;
the user must recheck rather than overwrite the remote document. The browser
persists the migration envelope before delivery, reuses its mutation ID on retry,
and refuses to flush unrelated pending mutations or discard dead-letter evidence.
Only remote-committed closes the migration preview and reports completion.
This operation and /api/workspaces/:workspaceId/mutations remain internal browser
protocols, not supported third-party integration surfaces.
This applies to V1 documents accepted by the current strict parser, not every
historical file. Missing legacy optional collections, allowedTypes, and
publication transition fields are normalized; unknown fields, invalid timestamps,
duplicate IDs, and dangling references reject the document. Gist discovery also
requires the exact description SubMan-Data and the recognized config filename.
The legacy browser snapshot path uses current snapshot validation and can
quarantine shapes that the V1 Gist parser accepts. Output files alone do not
reconstruct publish targets. See the
user migration guide for backup,
offline audit, V1 conversion, and manual recovery steps.
If a backup already exists but does not exactly match the current V1 file, the
write stops with migration_backup_conflict. Do not replace that backup until
the mismatch has been investigated.
A new Workspace starts with only subman.bootstrap.json. Its first coordinator
mutation creates V2 subman.json and deletes the bootstrap marker in the same
verified PATCH.
The browser database is subman-workspace, version 1, with one transactional
workspace-state root. It contains:
- The validated business snapshot and Workspace binding/baselines.
- Per-Workspace ordered mutations plus retry, blocked, and dead-letter metadata.
- Dispatcher leases and the next fencing token.
- Safe quarantine metadata and separately inaccessible repair payloads.
- Migration phase and timestamps.
GitHub tokens are never fields in this database. Session storage is the default; localStorage is used for a token only after Remember token is explicitly enabled. That persistent choice remains readable to same-origin JavaScript and therefore does not protect against active XSS.
Initialization imports subman:state:v1, subman:workspace-state:v2,
subman:workspace-mutation-queue:v1, and legacy quarantine records. Each of
not-started, copied, validated, and confirmed is a transaction boundary.
Identity, mutation schema, unique IDs, and contiguous expected revisions are
validated before confirmation. Invalid legacy records are quarantined rather
than silently discarded.
Before confirmation, untouched legacy keys are rollback sources. After
confirmation, the legacy keys are removed and IndexedDB retains the validated
cache and technical migration evidence. Cloud subman.json remains the
authoritative business document. If IndexedDB is unsupported, upgrade fails, quota is
exhausted, a transaction aborts, or stored data is corrupt, keep the application
in invalid-local-storage read-only repair mode. Do not clear storage, create a
new localStorage fallback, or resume delivery until the cause and retained
metadata have been inspected.
Queue inspection groups technical evidence by original Workspace identity and reports active, total, orphan, blocked, and dead-letter counts. Retained orphan queues may come from earlier versions. Removing Workspace switching must not delete, rebind, or automatically deliver that evidence. It remains inspectable until an explicit whole-queue discard or a separate operator recovery.
All runtime surfaces derive the same metrics from the persistence record:
-
activeQueueCountis the pending mutation count for the active Workspace. -
totalQueueCountis the pending mutation count across every Workspace. -
orphanedWorkspaceCountcounts each non-active Workspace that retains pending, blocked, or dead-letter evidence. -
blockedMutationCountcounts Workspace queues with blocked mutation metadata. -
deadLetterCountis the total retained dead-letter count. -
Sync from Cloud first resumes eligible pending delivery with the original IDs, then loads the latest cloud document. Retry timing and blocked dispositions remain authoritative; an unresolved queue prevents a successful refresh.
-
Reload from Cloud requires confirmation before abandoning all active pending changes. It validates the original Gist and atomically replaces the complete queue and cached snapshot with the latest cloud state.
-
Retained previous queues show only safe recovery metadata. Their explicit discard removes the complete selected queue; they cannot switch the current binding or become an alternative business mode.
-
Repair uses complete validated sequences and compare-and-swap checks for the current snapshot, binding, and queue. A cloud refresh racing a new edit must reject and re-read instead of silently dropping the new mutation.
-
Quarantine freezes corrupt queue data and retains only safe metadata in normal inspection and diagnostics.
State conflicts retain the trusted latest document and pending work. The user may explicitly discard the complete queue and reload cloud data, then reapply an edit against the new revision. There are no local overwrite or merge controls. Domain conflicts and repair evidence remain blocked until resolved or explicitly discarded; timestamps never authorize overwrite and remote tombstones stay protected. Logout clears authentication and stops delivery without deleting the binding, cache, pending mutation identities, or recovery evidence.
Each Workspace dispatcher acquires a persisted lease with a random owner ID, monotonic fencing token, expiry, and heartbeat. It checks the fence before a request and after every await. An expired or superseded owner cannot send the next mutation. Web Locks may reduce contention and BroadcastChannel may wake peers, but neither is required for correctness.
Retryable network, timeout, HTTP 408/429, and GitHub 5xx failures retain the same
mutation ID and use persisted bounded exponential backoff with jitter. Safe
Retry-After and rate-limit reset metadata provide the lower bound. GitHub
metadata, truncated raw-file reads, PATCH requests, and browser mutation requests
have explicit timeouts so one stalled operation cannot hold the queue forever.
Authentication failures stop delivery until reconnect; permanent upstream,
domain, queue-corruption, and operator-repair failures are never treated as
generic 5xx retries.
| Disposition | Typical cause | Operator action |
|---|---|---|
state-conflict |
Revision/identity conflict with a trusted latest document | Review pending evidence; confirm complete-queue discard and reload cloud data. |
domain-conflict |
Duplicate resource, output ownership, or existing entity | Edit, discard, or realign the blocked mutation. |
auth-required |
GitHub 401/403 or missing/revoked browser token | Reconnect; do not 5xx-retry. |
queue-corruption |
Reused mutation ID, invalid response, or revision gap | Freeze and explicitly repair/quarantine the queue. |
operator-repair |
Backup, recovery, commit-index, journal, or unknown internal failure | Remain read-only and follow the runbook. |
retryable-upstream |
Network, timeout, 408, 429, or GitHub 5xx | Keep the queue and use persisted backoff. |
permanent-upstream |
GitHub 404 or non-domain 409/422 | Stop delivery and repair Workspace/upstream state. |
invalid-request |
Invalid JSON/schema/mutation or a request/domain limit | Correct the input; do not retry unchanged data. |
Only state-conflict may include a validated latest document and revision. Error
responses expose stable codes/dispositions and bounded gateway metadata, never
GitHub bodies, credentials, arbitrary exception messages, or stacks.
Incoming JSON must use application/json or a +json media type and is limited
to 9 MiB, including streamed/chunked bodies. Stable failures are 415
unsupported_media_type, 400 invalid_json, and 413 payload_too_large.
Current UTF-8/count limits are: output 1 MiB, node raw 16 KiB, subscription URL
8 KiB, name 256 bytes, label 128 bytes, external key 256 bytes, 64 tags per
entity, 5,000 entities per collection, 1,000 rename entries/64 KiB per rename
map, and 8 MiB for canonical serialized subman.json. New and edited values must
fit. Unchanged oversized legacy fields remain readable and can be reduced;
tombstones above 10,000 per collection produce an observability warning but are
not rejected or compacted.
For external-key node writes, the resulting external:<key> tag also consumes
label bytes and a tag slot: new/changed keys fit 119 UTF-8 bytes and leave room
for at most 63 caller tags. The 256-byte mutation key limit alone does not make
a new key admissible.
Diagnostics export only counts; safe Workspace, revision, and mode metadata; mutation ID/kind/revision/time plus payload byte length and SHA-256; retry and disposition metadata; and quarantine key/bytes/time. They never read quarantine raw values or export proxy/subscription data, output content, complete documents, tokens, arbitrary errors, messages, or stacks.
Production responses must carry CSP with frame-ancestors, Referrer-Policy,
X-Content-Type-Options, and Permissions-Policy. The first-paint theme script must
use the build-tested nonce/hash path. Treat missing headers, a blocked theme
script, or persistent-token copy that omits active-XSS risk as a release failure.
The pre-deployment gate proves retry idempotency, SQLite credential exclusion, the Worker-to-Durable-Object path, and reserved-file enforcement. For the controlled production Workspace, verify:
-
subman.jsonhasversion: 2,schemaVersion: 2, the expectedworkspaceId, and a positiverevision. -
subman.v1.backup.jsonis byte-for-byte equal to the pre-migration V1 export. - A browser publication and a Server API node update both remain present.
- API responses contain no unrelated Workspace data or credentials.
- Browser diagnostics contain no canary payloads, quarantine raw values, tokens, arbitrary error messages, or stacks.
- A two-tab lease test proves one sender, expiry takeover, stale-fence rejection, and persisted retry timing without relying on Web Locks/BroadcastChannel.
- Exact-limit and one-byte-over tests cover request and domain limits; generated Worker responses contain the required security headers.
- Repeating the same controlled external-key Server API operation keeps one node with the same identity. Each accepted HTTP request may still advance the Workspace revision because it has a new mutation ID and update timestamp.
Rollback is an operator-controlled disaster-recovery procedure requiring separate approval and an explicit restoration plan. It is not routine browser repair, a way to bypass revisions/tombstones, or a Wrangler migration reversal. The historical compatibility artifact below must be revalidated before use:
For browser persistence, an interruption before confirmed may restart technical
migration from the untouched legacy keys. After confirmation and cleanup, do not recreate split-key
writes: retain/export safe migration metadata and recover the last validated
snapshot from IndexedDB. Tokens follow their independent session/persistent
choice and are not part of persistence rollback.
-
Stop or disable V2 writers so a compatibility Worker cannot race the coordinator.
-
Export the current V2
subman.jsonfor forward recovery. -
Restore the exact
subman.v1.backup.jsoncontent assubman.json. -
In an isolated worktree at
c89a60b, run its complete gate and deploy it as a normal forward deployment. This artifact restores V1 application behavior while retaining the appliedv1migration,WORKSPACE_COORDINATORbinding, and exportedWorkspaceCoordinatorclass:git worktree add --detach /tmp/subman-v1-compat c89a60b cd /tmp/subman-v1-compat bun install --frozen-lockfile bun test bun run check bun run lint bun run test:cf bun wrangler deploy --dry-run bun run deploy
-
Verify the V1 UI against the restored Gist before reopening writes.
Do not deploy or use Wrangler rollback to a pre-Durable-Object Worker version: Cloudflare Durable Object migrations are atomic lifecycle changes, and that older bundle lacks the required binding and class export. Keep the Durable Object namespace and SQLite records in place. V2-only edits cannot be translated back to V1 automatically; retain the V2 export for later reconciliation.
If V1 writes resume, the restored subman.json will eventually diverge from
the immutable original subman.v1.backup.json. Before a later V2 re-upgrade,
stop V1 writers, export both files, archive the original backup outside the
reserved filename, and deliberately remove the stale backup from the Gist. The
next controlled coordinator mutation will create a new byte-exact backup of the
current V1 document. Never replace or delete the reserved backup as an automatic
recovery action.
SubMan Wiki
| English | 简体中文 |
|---|---|
| User guide | 使用指南 |
| Development | 开发指南 |
| Server API | 接口文档 |
| Operations | 运维与恢复 |
| Migration | 旧版迁移 |
| sing-box export | sing-box 导出 |
| CI/CD | CI/CD |
| Roadmap | 路线图 |
| Changelog | 变更记录 |