parley

Deployment

Nothing here has been run against a real Cloudflare account yet. Each step is what the tooling documents; treat the first deploy as the verification and report anything that differs.

Requirements

Cost warnings before you start

Optional external databases (all free, no card)

Cloudflare D1 is the default database and file store. Each of these is switched on by setting its secrets on the Worker (dashboard: Workers, parley, Settings, Variables and Secrets). Unset means unused. Add secrets yourself; never commit them.

Service Secrets What it does Free limits (check current terms)
Turso TURSO_URL (libsql://...), TURSO_TOKEN Replaces D1 as the main database. The schema is created automatically on first request. Also stores files. about 5 GB. Every query becomes an internet round trip, so expect it to be slower than D1
Supabase Storage SUPABASE_URL, SUPABASE_KEY (service role key), optional SUPABASE_BUCKET (default parley, create it as a private bucket) Stores files up to 16 MiB about 1 GB, 50 MB per object
Firebase Firestore FIREBASE_SA (the whole service account JSON, Firestore in Native mode on the free Spark plan) Stores files up to 16 MiB in 512 KiB documents about 1 GiB, 20k writes per day

Files are placed on one of the configured stores by a hash of the file key, not by free space, and the store is recorded in the file’s key. Switching Turso on does not move existing data from D1.

None of these three adapters has ever run against the real services. They are tested only against local stand-ins that follow each service’s documented protocol (npm run test:store, and the API suite with test/fakes.mjs). Expect to debug the first real run.

Steps

  1. Sign in the CLI. This opens a browser for you to approve; do it yourself.

    npx wrangler login
    
  2. Create the database and put its id in wrangler.toml.

    npx wrangler d1 create parley
    

    Add the printed database_id = "..." line under [[d1_databases]]. Recent Wrangler versions can also create bindings automatically on first deploy; that path has not been tried here.

  3. Nothing to create for files: they use D1.

  4. Apply the schema.

    npm run migrate:remote
    
  5. Optional: voice relay. In the Cloudflare dashboard open Realtime, TURN, and create a key. Then:

    npx wrangler secret put TURN_KEY_ID
    npx wrangler secret put TURN_API_TOKEN
    

    Never commit these. .dev.vars is git-ignored for local use; .dev.vars.example lists the names.

  6. Deploy.

    npm run deploy
    
  7. Check it.

    curl https://<your-worker>.workers.dev/api/health
    

    Then open the URL, register, and keep the recovery code.

  8. Make yourself an admin. Add your username to ADMIN_USERNAMES in wrangler.toml (comma separated), then npm run deploy again. The admin panel is in the status menu under your name.

Optional sign-in providers

Each provider is offered on the sign-in screen only when both of its secrets exist. Register an OAuth app with the provider, set its callback (redirect) URL to https://YOUR-HOST/api/auth/oauth/PROVIDER/callback, then add the secrets on the Worker.

Provider Secrets Scope requested Callback path
GitHub OAUTH_GITHUB_ID, OAUTH_GITHUB_SECRET read:user /api/auth/oauth/github/callback
Google OAUTH_GOOGLE_ID, OAUTH_GOOGLE_SECRET openid profile /api/auth/oauth/google/callback
Discord OAUTH_DISCORD_ID, OAUTH_DISCORD_SECRET identify /api/auth/oauth/discord/callback

Only the provider’s user id and display label are stored. These flows are tested against a local fake provider (npm run test:signin with the .dev.vars.example values pointed at it), not against GitHub, Google or Discord themselves, so expect to debug the first real run. Passkeys need no setup but only work on the exact host they were registered on.

Configuration

Variable Default Meaning
MAX_UPLOAD_MB 25 Largest file sent in a single request (the free plan request body limit is 100 MB).
MAX_FILE_MB 25 Largest file via multipart upload (16 MiB parts).
USER_QUOTA_MB 100 Total attachment storage per user, including unfinished uploads. D1 free storage is 5 GB in total, shared with messages, so size this to your member count.
PBKDF2_ITERS 100000 Password hashing cost. 100,000 is the maximum Workers’ WebCrypto accepts. Stored per hash.
REGISTRATION_OPEN true Set "false" to close signups.
ADMIN_USERNAMES empty Comma separated usernames that get the admin panel.
POLLINATIONS_KEY unset Optional Pollinations API key (a Worker secret) used by the Experimental AI features when a user has not pasted their own. Pollinations rejects most anonymous requests.
OWNER_USERNAME ParleyOfficial The one account that gets the owner Admin page in settings, including the database editor. Owner implies admin.
OPENROUTER_API_KEY unset Optional Worker secret from openrouter.ai/keys. Admin Server studio sends its request to OpenRouter with it; without it, studio reports that the key is missing. Without OPENROUTER_MODEL the Worker reads OpenRouter’s public model list, picks the strongest free models by size and context window (cached for an hour) and tries up to four in order. Set OPENROUTER_MODEL to pin one.
KLIPY_API_KEY unset Optional Worker secret. A free key from the KLIPY partner dashboard turns on KLIPY search and trending in the GIF picker (npx wrangler secret put KLIPY_API_KEY). Written from KLIPY’s published response format and not tested against the live service, because that needs a key. Takes priority over GIF_API_BASE. Without any key, GIF search uses the built-in keyless sources.
GIF_API_BASE unset Optional. Base URL of a Tenor v2 compatible GIF API. Without it, GIF search needs no key: it uses reaction GIFs from otakugifs.xyz, purrbot.site and nekos.best reaction GIFs plus Wikimedia Commons search. That is a much smaller library than Tenor. Google shut the Tenor API down on 30 June 2026.
GIF_API_KEY unset Optional API key (a Worker secret) for a provider set in GIF_API_BASE. TENOR_API_KEY is accepted as an alias.
GIF_MEDIA_HOSTS tenor.com,klipy.com,giphy.com,cdn.otakugifs.xyz,cdn.purrbot.site,nekos.best,upload.wikimedia.org Comma separated host suffixes the server will download GIFs from. Add your provider’s media host here.
EXPERIMENTAL_USERNAMES empty Comma separated usernames that see the Experimental settings tab, in addition to the first account and anyone an admin enables. Set it as a plain variable in the Cloudflare dashboard.

Secrets, all optional:

Set secrets with wrangler secret put NAME. Never commit them. Run npm run migrate:remote to apply the migrations (the Workers Builds deploy command does this for you). A cron trigger aborts multipart uploads left open for 24 hours.

Deploying from GitHub

Two options:

Custom domain

Add a route or custom domain to the worker in the dashboard. Cookies are Secure automatically on https, and WebSocket and API origin checks compare against the request’s own origin, so no extra setting is needed.

Troubleshooting

Workers AI binding

wrangler.toml declares [ai] binding = "AI" for the image part of the explicit content scanner. Workers AI has a free daily allowance on the free plan. wrangler dev cannot start with that binding when you are not logged in to Cloudflare, so npm run dev writes a copy of the config without it (wrangler.local.toml, ignored by git) and the image check is simply off locally. To run the integration tests start the server with npx wrangler dev -c wrangler.local.toml --port 8787 --test-scheduled.