Silverbullet deploys to two places at once: the client to Cloudflare Workers, and the API, queue, and cron apps as Docker containers on a dedicated server managed by Coolify.
The automated path
Deployment is driven by .github/workflows/deploy-coolify.yaml. It runs on a push to main (production) or staging (staging), and can also be triggered manually with workflow_dispatch.
On each run it:
Builds Docker images for the server apps from
deploy/{api,queue,cron}/Dockerfile-liteand pushes them to Docker Hub.Runs database migrations over an SSH tunnel.
Triggers a Coolify redeploy for each server app.
Builds and deploys the client to Cloudflare.
Enable deploys in a new project
Projects created by new-project.sh ship with deploys disabled on purpose, so a first push cannot deploy to infrastructure that does not exist yet.
The scaffolding leaves you with two workflow files. The original deploy-coolify.yaml is disabled with branches: [DISABLED_DEPLOY] and kept as the target for template updates. A copy named <name>-coolify.yaml targets your renamed client app and is also disabled.
When your infrastructure is ready, edit <name>-coolify.yaml and set branches back to [main, staging]. Leave the original file disabled.
Configure GitHub Actions secrets
Secrets are injected at build time from Settings → Secrets and variables → Actions. You do not run wrangler secret put for backend values.
Single-value secrets, shared across environments:
BETTER_AUTH_SECRET MAILERSEND_API_KEY ENCRYPTION_KEY
S3_ACCESS_KEY S3_SECRET_KEY OPENAI_API_KEY
GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET
APPLE_CLIENT_ID APPLE_CLIENT_SECRET APPLE_APP_BUNDLE_IDENTIFIER
REDIS_HOST REDIS_USERNAME REDIS_PASSWORD
Per-environment secrets:
DATABASE_URL_PROD DATABASE_URL_STAGING
PADDLE_API_KEY_PROD PADDLE_API_KEY_STAGING
PADDLE_WEBHOOK_SECRET_PROD PADDLE_WEBHOOK_SECRET_STAGING
Infrastructure credentials:
CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_API_TOKEN
DOCKER_HUB_USERNAME DOCKER_HUB_PASSWORD
COOLIFY_TOKEN HETZNER_HOST HETZNER_SSH_PRIVATE_KEY
A missing secret usually surfaces as a build-time failure rather than a broken runtime, which is the failure mode you want.
Deploy the client manually
You can push the client without going through CI:
pnpm --filter=<name>-client deploy:staging
pnpm --filter=<name>-client deploy:production
Each script sets CLOUDFLARE_ENV, builds with Vite, and deploys with Wrangler. Always go through the package's own scripts — calling wrangler directly skips the environment setup and builds against the wrong configuration.
Before you ship
Run the workspace checks locally first, since the pre-commit hook only covers linting:
pnpm lint
pnpm test
pnpm build
Services to provision
Have these ready before your first production deploy: Postgres and Redis per environment, MailerSend, an S3 bucket with access keys, Google and Apple OAuth clients, a Paddle product with a webhook, an OpenAI API key, a generated BETTER_AUTH_SECRET, and Cloudflare account and API tokens.