- cloudflare: Secure serverless hosting
- cloudflare workers: Running the Hono rest API
- cloudflare pages - NextJS Frontend: NextJS15 frontend
- cloudflare D1: SQLite's database for pages and workers to query
custos/
├── README.md
├── database/
│ └── init.sql
├── frontend/
│ └── (Next.js application files)
├── workers/
│ └── api/
│ ├── package.json
│ ├── wrangler.toml
│ ├── src/
│ │ └── index.js
│ └── dist/
│ └── (compiled worker files)
└── terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
└── modules/
├── cloudflare-pages/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── cloudflare-workers/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── cloudflare-d1/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── cloudflare-dns/
├── main.tf
├── variables.tf
└── outputs.tf
| Path | Purpose |
|---|---|
terraform/ |
Root Terraform configuration plus modules for D1, Workers, Pages, and DNS. |
frontend/ |
Next.js application bundled with @opennextjs/cloudflare for Pages. |
workers/api/ |
Hono API worker targeting D1 and exposing /api/users, /api/posts, and health endpoints. |
database/ |
Cloudflare D1 assets and supporting SQL files. |
steps.md |
Field notes and caveats gathered during manual setup—mirrored throughout this README. |
The API is built using Hono on Cloudflare Workers, providing various endpoints to interact with the D1 database.
Base URL: http://api.custos.space (Note: Cloudflare typically redirects HTTP requests to HTTPS for security. Your API will most likely be accessed via https://api.custos.space).
-
Healthcheck:
GET http://api.custos.space/health- Checks the health and responsiveness of the API.
-
Users:
GET http://api.custos.space/users- Retrieves a list of users. Supports keyset pagination.
- Query Parameters:
limit: (Optional) Maximum number of users to return (default: 20, max: 100).cursor_id: (Optional) The ID of the last user from the previous page for pagination.
- Example (first 2 users):
http://api.custos.space/users?limit=2 - Example (next 2 users after ID 2):
http://api.custos.space/users?limit=2&cursor_id=2 GET http://api.custos.space/users/:id- Retrieves a single user by their ID.
- Example:
http://api.custos.space/users/1
- Active Cloudflare account with a domain delegate-able to Cloudflare DNS.
- Terraform ≥ 1.0.
- Node.js 20+ (or Bun) for the frontend and worker builds.
- Wrangler CLI (installed via
bun install --global wrangleror using the project-local dependency). - GitHub repository containing the frontend code (required for Pages deployment).
-
Delegate DNS to Cloudflare
- Add your domain to Cloudflare, update registrar nameservers, and wait until the zone status is Active.
- Record the zone ID (
cloudflare_zone_id) and account ID (cloudflare_account_id).
-
Create a scoped API token
- Minimum scopes: Zone:Read, Zone:DNS:Edit, Account:Workers Scripts:Edit, Account:D1 Databases:Edit (or Write), Account:Cloudflare Pages:Edit, Account Settings:Read.
- Export it for Terraform (
export TF_VAR_cloudflare_api_token="<token>") or populateterraform/terraform.tfvars(avoid committing secrets).
-
Wire Cloudflare Pages to GitHub
- Install the Cloudflare Pages GitHub App and grant access to the repository referenced by
github_repo. - Ensure the
production_branch(defaultproduction) builds locally with the configured command (build_config.build_command).
- Install the Cloudflare Pages GitHub App and grant access to the repository referenced by
-
Local preparation
-
API worker
cd workers/api bun install bun run build:bundle # produces dist/worker.js for validation
The Terraform module expects
api_worker_script_pathto point at the compiled module (default./workers/api/src/index.js). Adjust the variable if you relocate the bundle. -
Frontend
cd frontend bun install bun run buildDevelopment uses Turbopack (
next dev --turbopack), but keep the production build command as plainnext build—do not enable Turbopack for the build script OpenNext deployments on cloudflare break with the turbopack flag enabled.
-
-
Configure Terraform variables
- Edit
terraform/terraform.tfvarsand set:cloudflare_account_id = "..." cloudflare_zone_id = "..." domain = "custos.space" project_name = "custos-frontend-production" project_name_staging = "custos-frontend-staging" github_repo = "owner/repo" production_branch = "production" production_branch_staging = "staging" api_subdomain = "api"
- Add optional maps for
frontend_environment_varsandapi_environment_vars.
- Edit
-
Apply Terraform
cd terraform terraform init terraform plan terraform applyThe apply will:
- Create the D1 database and attach it to the Worker.
- Upload the API Worker, bind the D1 database, and register routes at
https://<api_subdomain>.<domain>/*. - Create production and staging Cloudflare Pages projects, configure environment variables (including
NEXT_PUBLIC_API_URL), and connect builds to GitHub. - Create DNS records for the Pages project and API subdomain.
-
Verify the deployment
- DNS: In Cloudflare → DNS, ensure A/CNAME records exist for the domain and the API subdomain.
- Worker: Hit
https://api.<domain>/api/health(or/api/healthcheck) once DNS propagates. - Pages: Cloudflare → Pages → project → confirm the build succeeded and the site responds at
https://<domain>.
cloudflare_api_token(sensitive)cloudflare_account_id,cloudflare_zone_iddomain,api_subdomaingithub_repo,production_branch,production_branch_stagingbuild_config(build_command,destination_dir,root_dir) – adjust if your build output differs from.vercel/output/static.frontend_environment_vars,api_environment_vars– maps merged into the Pages project and Worker respectively.
CORS_ORIGIN(comma-separated list). Production defaults tohttps://custos.space, development to*; update via Terraform'sapi_environment_varsfor multiple origins.
NEXT_PUBLIC_API_URLis injected automatically by Terraform.- Wallet connectivity expects
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_IDreferenced infrontend/src/components/Providers/Web3ModalProvider.
cd workers/api
bun install
bun run devcd frontend
bun install
bun run devWhen running locally, set NEXT_PUBLIC_API_URL (for example via .env.local) so the app points to the dev worker or production endpoint.
- Build the frontend for Cloudflare before deploying:
cd frontend rm -rf .open-next npx opennextjs-cloudflare build bunx wrangler deploy -c wrangler.jsonc -e production - Use
bunfor consistency across local development and CI (bun install,bun run dev). - The Worker build script (
bun run build) performs a dry-run deploy for validation; review the generated bundle indist/if you change dependencies.
deploy-production.ymlauto-builds the frontend with Bun/OpenNext and deploys to the production worker on every push to theproductionbranch.deploy-staging.ymlruns for pushes and pull requests targetingstaging, deploying to the staging worker and optionally creating preview aliases per branch.protect-production.ymlfails any pull request intoproductionunless the source branch isstaging, enforcing thestaging → productionpromotion flow.- Protect the
productionbranch in GitHub (required reviews, status checks) so merges only happen via approved PRs fromstaging. Similarly, protectstagingto ensure features merge via pull requests before promotion.
-
Frontend routing: Confirm
wrangler.jsonclists the production routes:{ "env": { "production": { "name": "cloudflare-fullstack-frontend", "routes": ["custos.space/*", "www.custos.space/*"] } } } -
Monitoring: Tail production worker logs with
bunx wrangler tail -c wrangler.jsonc -e production --format pretty. -
API errors: The Hono app logs to
console.error; inspect the Wrangler tail output for stack traces and ensureCORS_ORIGINincludes the calling origin.
# Terraform
terraform fmt && terraform validate
terraform apply -target=module.cloudflare_dns
# Wrangler
bunx wrangler deploy -c workers/api/wrangler.jsonc -e production
bunx wrangler tail -c workers/api/wrangler.jsonc -e production --format pretty
# Health checks
curl -i https://custos.space
# API calls (If under attack mode is set in cloudflare you cannot make CURL calls through terminal)
curl -i https://api.custos.space/health" | jq .
curl "https://api.custos.space/users?limit=8&cursor_id=2" | jq .
curl "https://api.custos.space/posts?limit=8&cursor_id=2" | jq .Keep steps.md updated alongside infrastructure changes so this README remains accurate.
