Skip to content

Deployment

Soul Map Atlas deploy ke Cloudflare — Workers untuk API, Pages untuk web dan docs.

Platform

AppPlatformConfig File
apps/apiCloudflare Workerswrangler.jsonc
apps/webCloudflare Pageswrangler.jsonc (static)
apps/docsCloudflare Pageswrangler.jsonc (static)

Environment Variables

Project ini pisahin config vars (non-sensitive, di wrangler.jsonc) dan secrets (sensitive, di-set terpisah). Jangan pernah commit secrets ke repo.

TipeLokasiCara set
Varswrangler.jsoncenv.{development,production}.varsEdit file, commit
Secrets.dev.vars (local) atau wrangler secret put (production)Tidak di-commit

Config Vars (wrangler.jsonc)

Vars ini non-sensitive dan boleh di-commit:

VariableRequiredDescription
BETTER_AUTH_URLYesBase URL untuk better-auth (e.g. https://api.soulmapatlas.app)
BETTER_AUTH_COOKIE_DOMAINYesDomain untuk auth cookie (e.g. soulmapatlas.app)
BETTER_AUTH_TRUSTED_ORIGINYesComma-separated trusted origins
DOKU_IS_PRODUCTIONYes"true" untuk production, "false" untuk dev
LOG_LEVELNoLog level: debug, info, warn, error (default: warn)
RESEND_FROM_EMAILYesDefault sender email (e.g. noreply@soulmapatlas.app)

Secrets (wrangler secret / .dev.vars)

Secrets dan credentials wajib di-set terpisah. Referensi lengkap ada di apps/api/.dev.vars.example.

VariableRequiredDescription
DATABASE_URLYesDirect connection string ke Neon Postgres. Wajib untuk local dev dan drizzle-kit migrate.
BETTER_AUTH_SECRETYesSession encryption secret (generate: openssl rand -base64 32)
GOOGLE_CLIENT_IDYesGoogle OAuth client ID
GOOGLE_CLIENT_SECRETYesGoogle OAuth client secret
RESEND_API_KEYNoResend API key untuk transactional emails. Kalau kosong, email silent-fail.
BASE44_APP_IDYesBase44 app ID untuk InvokeLLM integration
BASE44_ACCESS_TOKENYesBase44 access token untuk InvokeLLM auth
BASE44_SERVER_URLNoBase44 server URL (default: https://base44.app)
DOKU_CLIENT_IDYesDoku merchant client ID
DOKU_SECRET_KEYYesDoku secret key untuk signature
DEEP_INSIGHTS_PRICENoHarga Deep Insights dalam IDR (default: 50000)

Bindings (wrangler.jsonc)

BindingRequiredDescription
HYPERDRIVENoHyperdrive binding untuk connection pooling. Kalau ada, DATABASE_URL jadi fallback.
SESSION_KVNoKV namespace untuk session cache invalidation setelah entitlement grant.

Cron Triggers

apps/api memakai Cloudflare Cron Trigger untuk payment reconciliation server-side.

Schedule

ScheduleHandlerPurpose
*/5 * * * *scheduled() in src/index.tsReconcile non-terminal payment transactions against Doku

Triggered for both env.development and env.production via wrangler.jsonc:

jsonc
"triggers": {
  "crons": ["*/5 * * * *"]
}

Reconciler Behavior

reconcilePendingPayments() (src/services/payment/reconcile-payments.ts):

  1. Overlap suppression (best-effort): acquires a KV key reconcile:lock (TTL 240 s, < cron interval 300 s) via SESSION_KV. Concurrent cron ticks that see the key already set skip immediately. Not a hard lock — the CAS stamp is the real correctness guard.
  2. Selects up to 200 transactions that have no entitlement_granted_at stamp and are in any of these states:
    • pending
    • expired with created_at within the last 24 hours (grace window anchored on creation time, not last-modified)
    • paid (catches rows orphaned by a crash between status flip and grant)
  3. Calls Doku API per transaction to get current status — processed in bounded-concurrency chunks of 8 via Promise.allSettled.
  4. Flips DB status if changed (pending → paid, etc.).
  5. Calls grantEntitlementForTransaction() — idempotent CAS via entitlement_granted_at IS NULL guard inside a DB transaction.
  6. Per-transaction errors are isolated: one failing Doku call does not abort the batch; the tally continues.

Idempotency: The entitlement_granted_at column is the ledger key. The CAS stamp (UPDATE ... WHERE entitlement_granted_at IS NULL RETURNING id) ensures exactly one caller grants the entitlement, regardless of how many concurrent cron ticks or webhook events race. The KV lock is best-effort deduplication only — not a replacement for the CAS guard.

Grace window: The 24h window for expired transactions is anchored on transactions.created_at (fixed at creation time), not updated_at (which drifts on every status change).

Email DNS Verification (Resend)

Untuk mengirim email dari domain soulmapatlas.app via Resend, DNS records berikut harus dikonfigurasi di registrar/domain provider:

Required DNS Records

1. SPF Record (TXT)

Type:  TXT
Name:  soulmapatlas.app
Value: v=spf1 include:_spf.resend.com ~all

2. DKIM Record (TXT)

Type:  TXT
Name:  resend._domainkey.soulmapatlas.app
Value: <provided by Resend dashboard after adding domain>

3. DMARC Record (TXT) — recommended

Type:  TXT
Name:  _dmarc.soulmapatlas.app
Value: v=DMARC1; p=quarantine; rua=mailto:dmarc@soulmapatlas.app

Setup Steps

  1. Login ke Resend dashboard
  2. Navigate to DomainsAdd Domain
  3. Masukkan domain: soulmapatlas.app
  4. Resend akan generate DKIM record values
  5. Tambahkan semua DNS records di atas ke domain provider
  6. Klik Verify di Resend dashboard
  7. Setelah verified, copy API key dan simpan sebagai RESEND_API_KEY di Cloudflare Workers secrets

Local Development

  1. Copy example file:
bash
cp apps/api/.dev.vars.example apps/api/.dev.vars
  1. Isi semua secret di .dev.vars (file ini di-gitignore, jangan di-commit).

  2. Minimal yang wajib diisi untuk local dev:

DATABASE_URL=postgresql://user:password@host:port/database
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
BETTER_AUTH_URL=http://localhost:8787
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
BASE44_APP_ID=your-base44-app-id
BASE44_ACCESS_TOKEN=your-base44-access-token
DOKU_CLIENT_ID=your-doku-client-id
DOKU_SECRET_KEY=your-doku-secret-key

Email sending akan silent-fail kalau RESEND_API_KEY tidak di-set (graceful degradation).

CI/CD

Pipeline direkomendasikan pakai GitHub Actions dengan cloudflare/wrangler-action:

yaml
- name: Deploy API
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    workingDirectory: apps/api
    command: deploy --env production

Database Migrations

Migrations di-apply via drizzle-kit migrate:

bash
cd packages/db
npx drizzle-kit migrate

Pastikan DATABASE_URL di-set di environment sebelum menjalankan migrate. Di CI/CD, gunakan secret DATABASE_URL untuk production database.

Selalu generate migration baru dengan drizzle-kit generate sebelum apply.

Secrets Management

Secrets tidak boleh di-commit ke repo. Cara set beda per environment:

Production

bash
wrangler secret put BETTER_AUTH_SECRET     --env production
wrangler secret put DATABASE_URL            --env production
wrangler secret put GOOGLE_CLIENT_ID        --env production
wrangler secret put GOOGLE_CLIENT_SECRET    --env production
wrangler secret put RESEND_API_KEY          --env production
wrangler secret put BASE44_APP_ID           --env production
wrangler secret put BASE44_ACCESS_TOKEN     --env production
wrangler secret put BASE44_SERVER_URL       --env production
wrangler secret put DOKU_CLIENT_ID          --env production
wrangler secret put DOKU_SECRET_KEY         --env production
wrangler secret put DEEP_INSIGHTS_PRICE     --env production

Atau set via Cloudflare Dashboard → Workers & Pages → Your Worker → Settings → Variables & Secrets.

Local Development

Semua secret di-define di apps/api/.dev.vars. File .dev.vars.example di-repo sebagai referensi lengkap. Copy dan isi:

bash
cp apps/api/.dev.vars.example apps/api/.dev.vars