Operations and deployment
Configure the Node control plane and Cloudflare edge without mixing credentials.
Operations and deployment
Deployment is a separate owner-approved action. These files provision no live services automatically. Use distinct production and sandbox secrets, provider accounts and edge configuration stores.
Database
Create PostgreSQL 17 on Neon in Frankfurt. Put the direct owner connection in DIRECT_URL for the
migration job only. Prisma CLI prefers that value and falls back to DATABASE_URL when it is absent.
Apply pnpm db:migrate; never substitute prisma db push for the integrity migrations.
After migration, provision a separate application LOGIN through the database administrator. It must
not own the database or tables, and must have NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOINHERIT.
Grant it database CONNECT and membership in frawl_runtime permitting SET ROLE, without ADMIN
option. The migration grants membership only to the migration owner; it deliberately does not create
application passwords or guess a production login name. For a login named frawl_app, the membership
grant is GRANT frawl_runtime TO frawl_app WITH SET TRUE, INHERIT FALSE, ADMIN FALSE.
Use only that login's pooled connection as the application's DATABASE_URL. Do not expose DIRECT_URL
to the deployed app. The driver's startup role selects frawl_runtime, but this does not remove the
underlying authority of the login: an owner credential is unsafe even when normal queries assume the
restricted role. The application login must not belong to an owner or other privileged role.
Verify the actual application login cannot assume the migration-owner role. A product query without
transaction context must return no rows; cross-tenant inserts must be refused. These checks are distinct
from merely checking current_user after an owner connection performs SET ROLE.
Migration generation with the installed Prisma 7.10 CLI exhibited a schema-engine argument defect
for migrate diff; the checked-in SQL is complete. Normal migrate deploy was exercised without a
local wrapper. Regeneration should be verified against the installed matching engine before use.
The click parent is partitioned monthly. Authenticated maintenance creates upcoming partitions, purges raw clicks older than 90 days, expires encrypted webhook bodies after 30 days, and removes metric buckets older than 25 months. Financial attribution evidence is retained independently. A merchant's statutory retention policy may require operator-approved configuration changes.
Node application
Deploy apps/saas (127.0.0.1/[::1]:3210 -> https://app.frawla.io), apps/marketing ([::1]:3211 -> https://frawla.io),
and apps/docs ([::1]:3212 -> https://docs.frawla.io) using the Node runtime (never Edge) with the root workspace installation
on server-wafi (/opt/frawl) behind Nginx TLS termination and Cloudflare Edge routing (frawl-edge-gateway, configured via apps/tracking/wrangler.edge-gateway.jsonc), or on a Node-compatible host such as Vercel.
Notice that Next.js 16 next start must run with --hostname localhost when behind a reverse proxy so next-intl middleware rewrites match the server's internal hostname.
When routing frawla.io and docs.frawla.io through frawl-edge-gateway to internal Nginx surface paths (/__frawl_surface/marketing/ and /__frawl_surface/docs/), enforce a shared EDGE_GATEWAY_SECRET (X-Frawl-Edge-Token) in Nginx so direct external requests to /__frawl_surface/* return 404, and configure Cloudflare set_real_ip_from + real_ip_header CF-Connecting-IP (plus X-Frawl-Client-IP forwarding) so rate limiting and audit logs see real visitor IPs.
To idempotently provision the ADR-0014 Diamond Cloud AI holding tenant (diamond-cloud) and its three official portfolio programs (mahalli-partners, cloud-gaming-creators, dlal-ai-agencies) for outbox -> KV publication, run pnpm --filter @repo/scripts bootstrap:holding.
The apps/saas frontend and /api run together. Configure the public URLs and empty-secret template values in the platform
secret store. Enable an operational mail provider for verification and recovery. OAuth is optional.
GET /api/health is liveness. GET /api/health?deep=1 probes the database. Monitor both.
OpenTelemetry starts from apps/saas/instrumentation.ts; configure an OTLP endpoint/headers or a
collector integration. API logs include request and trace IDs without raw webhook data.
Cloudflare tracking
Follow apps/tracking/README.md for KV bindings, queue and DLQ creation, secrets and local fixtures.
Build the SDK before the Worker. wrangler deploy --dry-run validates bundling without deployment.
The control plane publishes signed program/link configuration through transactional outbox work.
Cloudflare Queues may redeliver. The Worker acknowledges only after authenticated ingestion commits. A control-plane outage retains queued messages for retry. A redirect still reaches its allowlisted merchant destination when tracking cannot be recorded; the click collection API returns a retryable error.
Enable the scheduler only when CRON_SECRET, control-plane URL and the dedicated job cursor KV are
configured. The scheduler invokes bounded pages of /api/internal/jobs; recurring billing and
payout preparation use persisted state. Queued retry processing also has an HMAC-authenticated
/api/v1/internal/process endpoint. Monitor failures and the returned cursor rather than treating
an invocation as proof that the entire backlog has drained.
If switching a consumer to direct PostgreSQL access through Hyperdrive, disable query caching. Never cache permission or financial reads across writes.
Moyasar billing activation
Frawl uses its own Moyasar secret/public keys and webhook secret. Merchant integration credentials remain in their individual encrypted ProviderConnection records. Set recurring and USD capability flags only after the account supports both. No code silently substitutes SAR for USD.
Card tokenization is browser-to-Moyasar. Saved tokens are encrypted. Before retrying an uncertain payment, retrieve its fixed given_id. Plan changes take effect at renewal, with a new price-version consent. A 3DS challenge is surfaced to the user. Reconcile unknown attempts before manual intervention. Payment receipts are not represented as Saudi compliant electronic tax invoices; operator invoicing setup is an external activation requirement.
Backlog, replay and recovery
Alert on inbox/outbox dead letters, processing age, failed billing reconciliations, queue retention headroom and tracking failures. Review unsupported provider events instead of acknowledging a fictional normalized sale. Replay an inbox by its stored identity through the processing service; never create a new financial event ID to work around a retry.
Before release, perform a restore rehearsal to a separate PostgreSQL instance. Compare per-currency ledger sums, commission balances, payout reservations, payment confirmations and rebuildable metrics. Include the encryption key version in recovery inventory; a database-only backup cannot decrypt secrets. Detailed acceptance evidence belongs in the release checklist and verification artifacts.
pnpm verify:restore performs a consistent-snapshot dump of the isolated local frawl_dev or
frawl_test, restores it into a unique temporary database, compares ledger, reservation and aggregate
totals, then drops only that temporary database. Set PG_BIN_DIRECTORY to the PostgreSQL 17 binaries
when necessary. CI uses the same client version through PG_CLIENT_DOCKER_IMAGE=postgres:17.
This local drill does not replace the encrypted backup and secret recovery rehearsal on staging.