Deployment Guide
Cloudflare deployment, CI, secrets, and cost estimates.
Deployment architecture
Section titled “Deployment architecture”The API and the frontend deploy separately. The frontend calls the API over HTTPS; the API stores metadata in D1, user-uploaded objects in R2 (or any S3-compatible provider), and short-lived data such as free-mode credentials and rate-limit counters in KV.
Before you begin
Section titled “Before you begin”You need the following:
- A Cloudflare account (free or paid).
- The Wrangler CLI. Install it with
npm install -g wrangler, or callbunx wranglerthrough bun. - bun 1.3.14 or later, to run the workspace scripts.
- A GitHub account (optional), for the Pages Git integration and CI.
Log in to Cloudflare:
wrangler loginThe API source lives in workers/ and the frontend in frontend/; both share types from shared/.
Deploy the Workers API
Section titled “Deploy the Workers API”Create the Cloudflare resources
Section titled “Create the Cloudflare resources”-
Create the D1 database.
wrangler d1 create picumet-dbCopy the
database_idvalue from the output. -
Create the KV namespaces.
wrangler kv namespace create PICUMET_KVwrangler kv namespace create PICUMET_KV --previewCopy the
idandpreview_idvalues. -
Create the R2 bucket.
wrangler r2 bucket create picumet-storage
Configure wrangler.toml
Section titled “Configure wrangler.toml”Replace the placeholder IDs in workers/wrangler.toml with the real resource IDs, and set ENVIRONMENT to production:
name = "picumet-api"main = "src/index.ts"compatibility_date = "2024-11-01"compatibility_flags = ["nodejs_compat"]account_id = "your_account_id"
[vars]ENVIRONMENT = "production"APP_BASE_URL = "https://picumet.com"ALLOWED_ORIGINS = "https://picumet.com"
[[d1_databases]]binding = "DB"database_name = "picumet-db"database_id = "your_d1_database_id"migrations_dir = "migrations"
[[kv_namespaces]]binding = "KV"id = "your_kv_namespace_id"
[[r2_buckets]]binding = "R2"bucket_name = "picumet-storage"| Binding | Purpose |
|---|---|
DB | D1 relational database for metadata, quotas, shares, and logs. |
KV | Session revocation, rate-limit counters, free-mode credentials, and the seed marker. |
R2 | Default object storage for uploaded files. |
To bind the API to api.yourdomain.com, add a routes block and point the DNS record at Cloudflare with the proxy enabled:
routes = [ { pattern = "api.yourdomain.com/*", zone_name = "yourdomain.com" }]Deploy and verify
Section titled “Deploy and verify”-
Install dependencies and run a preflight build.
cd workersbun installbun run buildbun run buildrunswrangler deploy --dry-run --outdir=dist; it bundles the worker without publishing it. -
Deploy the worker.
bun run deploybun run deployrunswrangler deploy. -
Verify the health endpoints.
curl -s https://api.yourdomain.com/api/public/health/livecurl -s https://api.yourdomain.com/api/public/health/ready/api/public/health/livereturns{"service":"picumet-api","status":"ok"}./api/public/health/readyreturns200with"ready": trueafter seeding and503before initialization. In production the API rejects business requests with503 INITIALIZATION_REQUIREDuntil the seed completes.
Run database migrations
Section titled “Run database migrations”Apply migrations before you deploy the API so the schema stays in sync with the code. Migration files live in workers/migrations/; Wrangler applies them in filename order. Each file is idempotent and ships with a commented rollback script.
cd workersbun run db:migrate:localThis command runs wrangler d1 execute picumet-db --local --file=migrations/0001_initial.sql against the local D1 database.
Preview
Section titled “Preview”Run the same migration against the preview D1 database before touching production:
bunx wrangler d1 execute picumet-db --preview --file=migrations/0001_initial.sqlProduction
Section titled “Production”Production migrations require human review. Do not apply them automatically from CI.
-
Review the target migration file in
workers/migrations/. -
Apply the migration to the remote database.
bunx wrangler d1 execute picumet-db --remote --file=migrations/0002_add_parts_and_download_tokens.sql -
Alternatively, apply all pending migrations in order.
bunx wrangler d1 migrations apply picumet-db --remote -
Confirm that the readiness endpoint returns
ready: true.curl -s https://api.yourdomain.com/api/public/health/ready
Set secrets
Section titled “Set secrets”Store secrets with wrangler secret put; Workers encrypts each value and keeps it out of wrangler.toml. Generate strong random values first:
openssl rand -base64 32bunx wrangler secret put JWT_SECRETbunx wrangler secret put ENCRYPTION_KEYSet ADMIN_PASSWORD in production
Section titled “Set ADMIN_PASSWORD in production”Picumet ships no built-in default credentials. The first-run seed creates the initial administrator from ADMIN_PASSWORD; in production the API fails closed (returns 503) when the administrator is missing. Set this secret before or right after the first deploy:
bunx wrangler secret put ADMIN_PASSWORDProduction password rules:
- At least 12 characters.
- Contains both letters and digits.
- No fixed default: generate a unique value per environment.
| Secret | Required | Purpose |
|---|---|---|
JWT_SECRET | Yes | Signs JWT session tokens; use at least 32 random bytes. |
ENCRYPTION_KEY | Yes | AES-GCM key for encrypting storage provider credentials at rest. |
ADMIN_PASSWORD | Production | Initial administrator password; the API fails closed when it is missing. |
ADMIN_USERNAME | No | Initial administrator username (defaults to admin). |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM | No | Email delivery for verification and password reset. |
TURNSTILE_SECRET_KEY | No | Cloudflare Turnstile verification. |
ENVIRONMENT, APP_BASE_URL, and ALLOWED_ORIGINS are plain vars in wrangler.toml, not secrets.
Deploy the frontend
Section titled “Deploy the frontend”The frontend is a static Vite build. bun run build runs tsc -b && vite build and writes the output to frontend/dist.
Build it locally to verify:
cd frontendbun installbun run buildMethod 1: deploy with the Pages Git integration
Section titled “Method 1: deploy with the Pages Git integration”- Push the repository to GitHub.
- In the Cloudflare dashboard, create a Pages project and connect the repository.
- Configure the build:
- Framework preset: Vite
- Build command:
cd frontend && bun install && bun run build - Build output directory:
frontend/dist - Root directory:
/
- Set the environment variables:
VITE_API_BASE_URL:https://api.yourdomain.comVITE_TURNSTILE_SITE_KEY:your_site_key(optional)
- Click Save and Deploy.
Cloudflare rebuilds and deploys the site on every push to the production branch.
Method 2: deploy from the command line
Section titled “Method 2: deploy from the command line”cd frontendbun run buildbunx wrangler pages deploy dist --project-name=picumetPoint a custom domain
Section titled “Point a custom domain”| Type | Name | Content | Proxied |
|---|---|---|---|
| CNAME | @ | picumet.pages.dev | Yes |
| CNAME | api | Workers-managed | Yes |
.github/workflows/ci.yml runs on every push and pull request to main. It is a quality gate only: it never deploys. Deployment stays manual (wrangler deploy) or Cloudflare-side (the Pages Git integration).
The workflow runs with bun 1.3.14:
- Workers:
bun install --frozen-lockfile→bun run typecheck→bun run test - Frontend:
bun install --frozen-lockfile→bun run typecheck→bun run test→bun run test:coverage→bun run build
Concurrent runs for the same branch cancel each other (concurrency.cancel-in-progress). The workflow has no deploy step and no Cloudflare credentials; add automatic deploys only after you have a tested rollback path.
Environment variables and bindings
Section titled “Environment variables and bindings”| Variable | Type | Required | Description |
|---|---|---|---|
ENVIRONMENT | var | Yes | development or production; gates fail-closed seeding and rate limiting. |
APP_BASE_URL | var | Yes | Frontend origin, used in email links and CORS. |
ALLOWED_ORIGINS | var | Yes | Comma-separated CORS origins. |
JWT_SECRET | secret | Yes | JWT signing key. |
ENCRYPTION_KEY | secret | Yes | AES-GCM encryption key for provider credentials. |
ADMIN_PASSWORD | secret | Production | Initial administrator password; at least 12 characters with letters and digits. |
ADMIN_USERNAME | secret | No | Initial administrator username (defaults to admin). |
DEMO_PASSWORD | secret | No | Demo user password (development only). |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM | secret | No | Email delivery. |
TURNSTILE_SECRET_KEY | secret | No | Turnstile verification. |
Bindings in wrangler.toml: DB (D1), KV (KV namespace), and R2 (R2 bucket).
Cost highlights
Section titled “Cost highlights”The free tier covers a small deployment:
- Workers: 100,000 requests/day
- Pages: unlimited builds and traffic
- D1: 5 GB storage + 5 million row reads/day
- KV: 100,000 reads/day + 1,000 writes/day
- R2: 10 GB storage/month + 1 million class-A operations/month
For a 1,000-user deployment with 100 GB of storage, budget roughly $21.50/month (Workers ~$10, D1 ~$5, R2 ~$6.50). Set usage alerts in the Cloudflare dashboard before you scale.
Rollback and incident response
Section titled “Rollback and incident response”Migrations fail
Section titled “Migrations fail”If a migration fails or produces bad data, do not patch production in place.
-
Stop the deploy and roll the API back to the previous version.
wrangler rollback -
Review the failed migration; most migrations carry a commented rollback script.
-
Repair the data and reapply the corrected migration.
-
Redeploy and verify the readiness endpoint.
Quota drift
Section titled “Quota drift”The scheduled task reconcileQuotas recomputes used_storage and used_files from file_metadata on a timer. To force a reconciliation immediately, run the same query manually:
UPDATE user_quotasSET used_storage = COALESCE((SELECT SUM(size) FROM file_metadata WHERE owner_id = user_quotas.user_id), 0), used_files = (SELECT COUNT(*) FROM file_metadata WHERE owner_id = user_quotas.user_id)WHERE user_id = '<user_id>';Security incidents
Section titled “Security incidents”- Deploy the fix immediately (
wrangler deploy). - Audit
access_logsfor the affected window. - Revoke sessions and API keys as needed, then notify affected users.