Skip to content

GitHub Actions Deployment

EdgeChat's primary deployment method is GitHub Actions with Cloudflare Workers.

The workflow is located at .github/workflows/deploy-worker.yml and can complete the deployment automatically whenever code is pushed.

Required Configuration

SettingPurpose
CLOUDFLARE_API_TOKENAllows Actions to call the Cloudflare API to create, inspect, and deploy resources.
CLOUDFLARE_ACCOUNT_IDSelects the target Cloudflare account.
CFCHAT_ADMIN_USERNAMESets the administrator username created during the first deployment.
CFCHAT_ADMIN_PASSWORDSets the administrator password created during the first deployment.

Optional Configuration

SettingPurpose
CFCHAT_ADMIN_DISPLAY_NAMESets the administrator display name. The username is used when this value is omitted.
EDGECHAT_ENCRYPTION_KEYRINGProvides a complete server-side encryption keyring. One is generated automatically on the first deployment when omitted.

Triggers

  • Push to master or main.
  • Trigger workflow_dispatch manually from the GitHub Actions page.

Workflow Steps

  1. Check out the source code.
  2. Install dependencies.
  3. Build the frontend.
  4. Inspect or create Cloudflare resources.
  5. Generate the CI-specific wrangler.ci.toml.
  6. Initialize the database when D1 is created for the first time.
  7. Inspect and apply pending D1 migrations.
  8. Verify migration records and the complete structure against the generated schema manifest.
  9. Optionally create the administrator account.
  10. Inspect and prepare the Worker encryption Secret.
  11. Deploy the Worker.

R2 Attachment Support

The resource inspection step calls Cloudflare's official GET /accounts/{ACCOUNT_ID}/r2/buckets?per_page=1 endpoint to determine whether R2 is enabled for the target account.

  • R2 is enabled: the workflow reuses or creates the cfchat-files bucket and creates the FILES binding as usual.
  • Cloudflare returns error code 10042: the Worker is still deployed, but the FILES binding is removed from the CI configuration.
  • The Token lacks permission, the account ID is incorrect, or another Cloudflare API error occurs: deployment stops so a configuration problem is not mistaken for an account without R2.

Without R2, sign-in, text chat, and administration still work. Users who upload attachments see a message explaining that the current deployment has no R2 binding. Attachment downloads and Telegram file synchronization are also unavailable. After enabling R2, rerun Deploy Worker; the workflow idempotently creates or reuses the bucket and restores attachment support automatically.

R2 Cleanup Budgets and Site Origins

Each scheduled GC invocation defaults to at most 900 combined D1 API and R2 internal operations, 1,200 D1 SQL statements, and 300 R2 operations, leaving headroom below Cloudflare's invocation boundary. GC_BATCH_SIZE=90; cleanup categories process one batch per round, and dynamic SQL stays below D1's 100-bound-parameter limit. Work left when a budget is reached remains in D1 for the next Cron invocation. The default Cron runs daily, so a sustained backlog above 300 R2 objects per day should normally be handled by increasing Cron frequency before cautiously raising per-invocation budgets for the selected Workers plan.

New local site icons are normalized to an object key in D1. If an older deployment stored a complete https://your-site/files/... URL, list every trusted public origin in SITE_ORIGINS within wrangler.example.toml, separated by commas:

toml
SITE_ORIGINS = "https://chat.example.com,https://chat-alt.example.com"

Relative /files/... values and the current request origin are recognized automatically. Genuine cross-origin URLs remain external. When the origin of a historical complete URL cannot be established, GC conservatively retains the matching key instead of risking deletion. Existing D1 databases receive a new migration that replaces the old triggers, so deployments must continue to migrate and verify D1 before publishing the Worker.

Server-side Encryption Keys

By default, each key is stored in an independent, versioned Worker Secret. A separate active key ID indicates which version encrypts newly written data. Existing EDGECHAT_ENCRYPTION_KEYRING JSON keyrings remain supported. The manual format is:

json
{"activeKeyId":"v1","keys":{"v1":"BASE64_ENCODED_32_BYTE_KEY"}}
  • If the target Worker has no encryption Secret, Actions generates a random 32-byte AES key.
  • Normal pushes and ordinary manual deployments preserve existing Secrets without regenerating, overwriting, or rotating them.
  • New messages and attachments use the active key after deployment.
  • Telegram Bot Tokens and Webhook Secrets entered by administrators are encrypted with the same keyring before being stored in D1.
  • Historical D1 messages and R2 attachments are not bulk-migrated; reads remain compatible with both plaintext and ciphertext.

To specify the initial deployment key manually, add a GitHub Repository Secret with the same name before deploying.

To rotate an existing key automatically and incrementally:

  1. Run Deploy Worker manually.
  2. Enable rotate_encryption_key.
  3. The workflow adds a versioned key Secret and changes the active key ID.
  4. All older versioned Secrets and the older JSON keyring remain unchanged.

apply_encryption_keyring is a fallback manual override. When using it, the Repository Secret must contain the complete JSON keyring. Retain every old key ID still referenced by historical messages, attachments, or encrypted Telegram Bot Tokens, then add the new key and update activeKeyId. Deleting an old key makes the corresponding historical ciphertext unreadable. The two rotation options cannot be enabled during the same run.

Cloudflare does not allow Worker Secrets to be read back. Automatic rotation works around this restriction by adding new versions without reading or overwriting older ones. If you need full custody and backups of every key, provide a JSON keyring manually before the first deployment.

This is server-side encryption at rest, not end-to-end encryption. The Worker decrypts content after session authorization succeeds, so the deployer and Worker runtime remain inside the trust boundary. The admin panel no longer provides message-content search or full-conversation inspection.

Automatic D1 Migrations

Before publishing the Worker, every deployment inspects the actual tables, columns, indexes, and triggers in the target D1 database. The edgechat_schema_migrations table records applied migrations and their file checksums.

  • A new D1 database first runs the complete worker/schema.sql, then records the current migration baseline.
  • An existing D1 database runs only the worker/migrations/*.sql files whose structures are not already present.
  • Databases previously deployed with the old unread-badge implementation first merge channel_reads cursors into the current message_reads, then remove legacy foreign keys that would block rebuilding the messages table.
  • The Telegram Bridge migration independently creates configuration and mapping tables and adds external-sender, file-source, and deduplication fields to messages.
  • Applied migrations are not run again, preventing duplicate columns and accidental data replacement.
  • If a database contains only part of a migration, the workflow stops and reports the missing structure instead of allowing new code to connect to an outdated database and return repeated 500 errors.
  • Applied migration files must never be rewritten. Database changes require a new, later migration file and an updated migration manifest.

After a fork pulls upstream changes, Actions updates both the database structure and the Worker as long as the Cloudflare Secrets point to the correct account. There is no need to sign in to D1 and execute SQL manually.

Update Checks in the Admin Panel

Installation & Maintenance

Administrators can open /admin/maintenance from the separate entry at the bottom of the sidebar. Manually check D1 connectivity, migration records and actual tables/columns/indexes/triggers, KV Sessions, optional R2, all three Durable Objects, and required environment variables. The page also shows EdgeChat and database versions and exports a JSON diagnostic report without secret values. It never polls automatically, applies migrations, clears data, runs GC or resets configuration. Demo results are explicitly labeled as simulated.

Deployment and the admin page share the collection and assessment rules in worker/src/maintenance/schema-contract.ts. npm run schema:generate executes worker/schema.sql in SQLite, collects its structure and includes the existing migration list with SQL checksums in worker/src/generated/schema-manifest.json. This generated file is not committed; tests, the main web build and Wrangler custom builds generate it automatically for bundling. Future changes still require only the full schema, incremental SQL and the existing migration manifest, not a second admin checklist.

Custom deployment configurations should retain [build] command = "npm run schema:generate" from the template. For manual releases, after applying migrations and before deploying the Worker, use the same CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID and EDGECHAT_D1_DATABASE_ID as the migration script and run:

bash
node .github/scripts/prepare-d1-migrations.mjs --verify

Do not publish if verification fails. For missing migrations or ledger records, inspect logs and rerun normal deployment. For checksum mismatches, schema drift or unknown migrations, back up and verify versions first; do not edit the ledger or replay historical SQL. Extra legacy schema objects are not automatically removed.

Checks cover object and column existence, not column types, index definitions or business data integrity. KV/R2 probes are bounded reads, and DO health never schedules an alarm, so a healthy result does not verify writes, decryption, Cron scheduling or end-to-end message delivery. If D1/KV failure prevents login, use Cloudflare and deployment logs instead. Environment checks confirm presence only, not key validity or historical decryptability.

Version Updates

Production builds record the GitHub repository, branch, and commit used for the deployment. When an administrator opens "Site Settings," the browser calls the GitHub Compare API directly to compare the remote branch. The administrator can also click "Check for updates" to run the comparison again. This happens entirely in the frontend, creates no scheduled job, and requires no additional Cloudflare Secret.

For accurate results:

  • The source repository must remain public so the browser can read comparison data anonymously.
  • Actions deployments automatically use GITHUB_REPOSITORY, GITHUB_REF_NAME, and GITHUB_SHA.
  • Manual deployments should build from a clean commit that has already been pushed.
  • When there are uncommitted changes, unpushed commits, or temporary GitHub API rate limits, the admin panel reports that an accurate check is unavailable instead of falsely reporting an update.

Standalone Demo Workflow

The frontend-only demo uses .github/workflows/deploy-demo.yml and can only be triggered manually. It reads the separate DEMO_CLOUDFLARE_ACCOUNT_ID and DEMO_CLOUDFLARE_API_TOKEN, builds frontend/demo-dist, and deploys edgechat-demo.

This workflow does not inspect production resources, initialize or migrate D1, create an administrator, or prepare encryption Secrets. It also does not use the production Cloudflare Secrets. See Frontend-only Demo for the complete guide.

Android CI and Releases

.github/workflows/android-ci.yml continues to verify the temporarily deprecated native Compose client. It runs Gradle Wrapper validation, unit tests, Lint, and a Debug APK build when android/** or the API v1 contract changes.

.github/workflows/capacitor-android-ci.yml builds the primary Web UI Android client with Node.js 24, Java 21, and Android SDK 36. It builds and syncs the bundled frontend, runs application-module unit tests and Lint, builds the Debug and instrumentation APKs, and uploads the edgechat-capacitor-debug artifact. Its package name is com.aozorae.edgechat.web, so it can still be installed alongside the native client.

.github/workflows/android-release.yml runs from an android-v* tag or a manual trigger. It builds and syncs the Vue frontend, creates signed Capacitor APK/AAB artifacts and SHA256SUMS.txt, then publishes a GitHub Release. The native Compose client no longer enters this release workflow.

Configure these Repository Secrets before publishing:

  • ANDROID_KEYSTORE_BASE64
  • ANDROID_KEYSTORE_PASSWORD
  • ANDROID_KEY_ALIAS
  • ANDROID_KEY_PASSWORD

Back up the signing keystore offline. The workflow restores it from Secrets only for the duration of the job. Android-only and Android documentation changes do not trigger the production Worker deployment; backend, frontend, test, migration, and deployment-script changes still run the complete Cloudflare pipeline.

CLOUDFLARE_API_TOKEN requires at least:

  • Workers Scripts:Edit
  • D1:Edit
  • Workers KV Storage:Edit
  • R2:Edit

The current wrangler.example.toml does not configure Workers Routes. Its Cron Trigger is deployed with the Worker script, and Cloudflare does not expose a separate API Token permission named Cron Triggers. The default GitHub Actions workflow therefore does not require Workers Routes Write. Add that zone-level permission only if you customize the CI configuration with domain routes.

Keep R2:Edit even if the account has not enabled R2. The workflow needs this permission to distinguish reliably between "R2 is not enabled" and "the Token cannot access R2." The latter is treated as a configuration error and stops deployment.

Recommendations

  • Confirm that all Repository Secrets are present before the first deployment.
  • If resources already exist, verify their IDs and namespace configuration first.
  • Manage the administrator account through Secrets instead of editing the database manually.