Appearance
Deployment
Soul Map Atlas deploy ke Cloudflare — Workers untuk API, Pages untuk web dan docs.
Platform
| App | Platform | Config File |
|---|---|---|
apps/api | Cloudflare Workers | wrangler.jsonc |
apps/web | Cloudflare Pages | wrangler.jsonc (static) |
apps/docs | Cloudflare Pages | wrangler.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.
| Tipe | Lokasi | Cara set |
|---|---|---|
| Vars | wrangler.jsonc → env.{development,production}.vars | Edit 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:
| Variable | Required | Description |
|---|---|---|
BETTER_AUTH_URL | Yes | Base URL untuk better-auth (e.g. https://api.soulmapatlas.app) |
BETTER_AUTH_COOKIE_DOMAIN | Yes | Domain untuk auth cookie (e.g. soulmapatlas.app) |
BETTER_AUTH_TRUSTED_ORIGIN | Yes | Comma-separated trusted origins |
DOKU_IS_PRODUCTION | Yes | "true" untuk production, "false" untuk dev |
LOG_LEVEL | No | Log level: debug, info, warn, error (default: warn) |
RESEND_FROM_EMAIL | Yes | Default 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.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | Direct connection string ke Neon Postgres. Wajib untuk local dev dan drizzle-kit migrate. |
BETTER_AUTH_SECRET | Yes | Session encryption secret (generate: openssl rand -base64 32) |
GOOGLE_CLIENT_ID | Yes | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Yes | Google OAuth client secret |
RESEND_API_KEY | No | Resend API key untuk transactional emails. Kalau kosong, email silent-fail. |
BASE44_APP_ID | Yes | Base44 app ID untuk InvokeLLM integration |
BASE44_ACCESS_TOKEN | Yes | Base44 access token untuk InvokeLLM auth |
BASE44_SERVER_URL | No | Base44 server URL (default: https://base44.app) |
DOKU_CLIENT_ID | Yes | Doku merchant client ID |
DOKU_SECRET_KEY | Yes | Doku secret key untuk signature |
DEEP_INSIGHTS_PRICE | No | Harga Deep Insights dalam IDR (default: 50000) |
Bindings (wrangler.jsonc)
| Binding | Required | Description |
|---|---|---|
HYPERDRIVE | No | Hyperdrive binding untuk connection pooling. Kalau ada, DATABASE_URL jadi fallback. |
SESSION_KV | No | KV namespace untuk session cache invalidation setelah entitlement grant. |
Cron Triggers
apps/api memakai Cloudflare Cron Trigger untuk payment reconciliation server-side.
Schedule
| Schedule | Handler | Purpose |
|---|---|---|
*/5 * * * * | scheduled() in src/index.ts | Reconcile 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):
- Overlap suppression (best-effort): acquires a KV key
reconcile:lock(TTL 240 s, < cron interval 300 s) viaSESSION_KV. Concurrent cron ticks that see the key already set skip immediately. Not a hard lock — the CAS stamp is the real correctness guard. - Selects up to 200 transactions that have no
entitlement_granted_atstamp and are in any of these states:pendingexpiredwithcreated_atwithin 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)
- Calls Doku API per transaction to get current status — processed in bounded-concurrency chunks of 8 via
Promise.allSettled. - Flips DB status if changed (
pending → paid, etc.). - Calls
grantEntitlementForTransaction()— idempotent CAS viaentitlement_granted_at IS NULLguard inside a DB transaction. - 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 ~all2. 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.appSetup Steps
- Login ke Resend dashboard
- Navigate to Domains → Add Domain
- Masukkan domain:
soulmapatlas.app - Resend akan generate DKIM record values
- Tambahkan semua DNS records di atas ke domain provider
- Klik Verify di Resend dashboard
- Setelah verified, copy API key dan simpan sebagai
RESEND_API_KEYdi Cloudflare Workers secrets
Local Development
- Copy example file:
bash
cp apps/api/.dev.vars.example apps/api/.dev.varsIsi semua secret di
.dev.vars(file ini di-gitignore, jangan di-commit).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-keyEmail 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 productionDatabase Migrations
Migrations di-apply via drizzle-kit migrate:
bash
cd packages/db
npx drizzle-kit migratePastikan 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 productionAtau 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