Codex Taskboard can run as a small shared Cloudflare deployment for two trusted collaborators:
- one Worker serves the built UI and the JSON API;
- D1 is the authoritative business database;
- a private R2 bucket stores attachments;
- UI, API, and attachment routes use HTTPS Basic Authentication;
/healthis public; - open boards poll a global revision every two seconds and refresh after a change.
The production resource names are:
| Resource | Name |
|---|---|
| Worker | codex-taskboard |
| D1 database | codex-taskboard-db |
| R2 bucket | codex-taskboard-attachments |
This is intentionally a shared-password trust model. The Basic username is only the actor name displayed in task and comment attribution, not a verified identity. Anyone who knows the shared password has full read and write access and can choose any actor name. Use it only with the other trusted collaborator.
The cloud stores project, issue, comment, relation, workflow, and attachment data. It does not store a device's absolute project or worktree paths.
Each collaborator runs the local companion for Codex, Git/worktree scanning, installed Skill/MCP discovery, and project path mapping. The companion keeps the cloud URL, actor name, shared password, and device-specific project mappings in .data/cloud-companion.json with mode 0600.
When cloud mode is active, the cloud is the only business-data source. A failed cloud request fails visibly. The companion does not fall back to the local SQLite database and does not write to both databases. taskctl cloud logout returns that device to its separate local mode; it does not merge local and cloud data.
Install dependencies and build the frontend:
npm ci
npm run build:webCreate an ignored .dev.vars file containing a local-only value for TASKBOARD_SHARED_SECRET, apply the D1 migration to Wrangler's local state, and start the Worker:
npm run cloud:migrate:local
npm run dev:cloudOpen the printed loopback URL. The browser shows its native Basic Authentication prompt. Enter any local actor name as the username and the value from .dev.vars as the password.
Local Wrangler state lives under .wrangler/ and is not committed.
Authenticate Wrangler first:
npx wrangler login
npx wrangler whoamiProvision the production D1 database and private R2 bucket using the exact names above.
npx wrangler d1 create codex-taskboard-db
npx wrangler r2 bucket create codex-taskboard-attachmentswrangler.jsonc contains one production configuration and identifies the D1 binding by its resource name and database_id. A D1 database ID is public metadata and does not grant access, so it can be committed. Wrangler local development creates persistent local equivalents under .wrangler/; those are local simulations, not additional Cloudflare environments.
Apply the remote D1 migration and validate the deployment bundle:
npm run cloud:migrate
npm run cloud:deploy:dry-runSet the shared password through Wrangler's private interactive prompt after the database schema is ready. Do not put the value in wrangler.jsonc, a shell command, a log, or a committed file. Then deploy the production Worker:
npx wrangler secret put TASKBOARD_SHARED_SECRET
npm run cloud:deployThese commands create or update Cloudflare resources. This repository contains the production D1 database ID for the binding, but it does not contain the shared password or any API or OAuth token. Keep those credentials out of Git; cloning the repository does not grant access or mean the Worker has already been deployed.
Give the other collaborator the deployed Worker HTTPS origin and shared password through a trusted channel. Never publish the password in the repository, an issue, or logs.
Current Cloudflare references:
The owner follows this device setup too, using the owner's own actor name and checkout path. The friend does not need your local database or your filesystem paths. They update their existing clone and build the current UI:
git pull --ff-only
npm ci
npm run build:webStart the local companion:
CODEX_TASKBOARD_HOST=127.0.0.1 npm startIn a second terminal, configure cloud mode. Use the deployed HTTPS Worker origin, choose the actor name that should appear on their actions, and enter the shared password only at the private Shared key: prompt:
npm run taskctl -- cloud login \
--url https://YOUR-WORKER-ORIGIN \
--actor-name "FRIEND-DISPLAY-NAME"
npm run taskctl -- cloud status
npm run taskctl -- project listThe shared password is not part of the command and is not echoed by the prompt.
For every cloud project used with Codex, map its project ID to that friend's own absolute checkout path:
npm run taskctl -- project map PROJECT_ID \
--workspace-path /absolute/path/on/their/deviceThe owner runs the same mapping command with the owner's own path. Mappings are intentionally different on each device and are never synchronized to D1.
Launch the injected Codex window:
CODEX_TASKBOARD_HOST=127.0.0.1 npm run codexnpm run codex reuses or starts the loopback companion. Keep it running while using the embedded board. The companion supplies local Codex/Git/Skill/MCP capabilities and sends the shared password to the Worker only in the HTTPS Basic Authorization header. It does not write that password to D1 or R2, return it to the browser UI, or print it in logs. Device paths also stay off Cloudflare.
Do not point CODEX_TASKBOARD_URL directly at the cloud origin for this workflow. taskctl talks to the loopback companion, which applies Basic Authentication and the device's local project mapping. If the companion uses a non-default loopback port, set CODEX_TASKBOARD_COMPANION_URL to that loopback origin.
Either collaborator can open the deployed HTTPS Worker URL directly. The browser's native Basic Authentication prompt asks for:
- username: the actor display name for that browser;
- password: the shared password.
The browser view supports the shared board and attachments. Device-only Codex, Git/worktree, Skill, and MCP capabilities still require the local companion.
The owner rotates the Worker secret using Wrangler's interactive prompt:
npx wrangler secret put TASKBOARD_SHARED_SECRETAfter rotation, both devices rerun taskctl cloud login and enter the new password. Browser-only users must authenticate again; closing the authenticated browser session or clearing site authentication may be necessary because browsers cache Basic credentials.
Because both collaborators share one password, rotation affects both at once. There is no individual-user revocation in this two-person trust model.
The migration tool takes a consistent SQLite snapshot with VACUUM INTO, removes structured device-only paths, exports attachment hashes, and writes a private bundle. The default local paths are:
npm run cloud:data -- export \
--database .data/taskboard.sqlite \
--attachments .data/attachments \
--output cloud-migration-exports/initialThe output directory contains issue content and attachment bytes. It is mode-restricted and ignored by Git, but it must still be handled as private data. This export is optional when starting with an empty cloud board.
Before importing, authenticate Wrangler, provision the named D1 and R2 resources, and run npm run cloud:migrate so the remote D1 schema exists. The target D1 must contain no projects, and none of the bundle's attachment keys may already exist in R2. Import refuses a non-empty target instead of merging or overwriting it.
Run the one-time Wrangler adapter with an explicit remote-operation acknowledgement:
TASKBOARD_MIGRATION_REMOTE=1 npm run cloud:data -- import \
--bundle cloud-migration-exports/initial \
--adapter ./scripts/wrangler-cloud-adapter.mjs
TASKBOARD_MIGRATION_REMOTE=1 npm run cloud:data -- verify \
--bundle cloud-migration-exports/initial \
--adapter ./scripts/wrangler-cloud-adapter.mjsTASKBOARD_MIGRATION_REMOTE=1 is a deliberate safety gate for these two commands. The adapter uses the current Wrangler login and the production resource names from wrangler.jsonc; it does not add a migration HTTP endpoint or store Cloudflare credentials. The commands are not run automatically by deployment, so having the repository does not mean data has already been imported.
The adapter has a local-persistence integration test that does not access remote Cloudflare resources:
node --test test/cloud-migration.test.mjs