Tech Stack
This section is being locked item by item via discussion. Each item below shows the Decision, the Why, and the Field signal (what comparable compliance SaaS run today, based on 2025–2026 research across Vanta, Drata, Scytale, Secureframe, Sprinto, Hyperproof, Wiz, Snyk, Thoropass).
1. Frontend — ✅ Locked
Decision: React Single-Page Application (SPA), built with Vite, written in TypeScript.
Why we chose this:
- SPA over SSR / Next.js — the S.E.T app lives entirely behind a login wall. SSR's main wins (SEO crawling, fast first paint for anonymous visitors) don't apply once users must sign in to see anything. A SPA keeps the frontend a pure presentation layer over our NestJS BFF, avoiding the "two server runtimes" redundancy of pairing Next.js with a separate backend.
- React over Vue / Svelte / Angular — universal in the compliance/security SaaS field. Our locked shadcn/ui + Tremor component libraries (from the UI/UX section) are React-only, so this is doubly reinforced. Largest hiring pool by a wide margin.
- Vite over Webpack / Rspack — the 2026 default build tool. Extremely fast dev server, simple config, native ESM. Webpack is legacy and slow; Rspack is "if you outgrow Vite" — won't happen any time soon.
- TypeScript over JavaScript — industry-universal for serious SaaS. Catches errors at build time (critical for a correctness-sensitive security product) and unifies the language with the NestJS backend: define a
Findingtype once, share it across the stack.
TypeScript profile (frontend + backend): full strict mode — strict: true + noImplicitOverride + noUncheckedIndexedAccess + exactOptionalPropertyTypes. Documented and enforced in the Secure Coding Standard §4.
Field signal: 9 of 9 compliance/security SaaS researched use React + TypeScript on the frontend. Vanta, Wiz, Snyk, Hyperproof run SPAs; Drata, Sprinto run Next.js. For S.E.T's BFF architecture and login-walled app, SPA is the cleaner choice.
2. Backend — ✅ Locked
Decision: NestJS with the Fastify adapter, written in TypeScript, on Node.js.
Why we chose this:
- NestJS over Express / Fastify alone — NestJS provides real structure for the codebase: modules, dependency injection, controllers, services, guards, interceptors, pipes. S.E.T's substantial business logic — multi-tenant isolation, evidence pipelines, finding lifecycles, framework mappings, task management, statistics — benefits enormously from that structure. Express and Fastify alone are minimal and force every team to hand-roll its own architecture.
- Fastify adapter over Express adapter — NestJS runs on top of either Express or Fastify under the hood. Fastify is ~2–3x faster in raw benchmarks (~30k RPS vs ~12k for Express) with a more modern API. We get NestJS structure and Fastify speed.
- Node.js / TypeScript over Go or Ruby on Rails — keeps the language unified with the frontend (shared types, single skill set, easier hiring). Drata — our closest field analog — runs Node.js. Go is reserved for performance-heavy cloud-asset scanning (the Wiz pattern), which is not our profile. Rails (Secureframe's choice) means a different language, smaller modern hiring pool, and slower runtime.
Honest tradeoff: a Go backend would handle ~5–10x more raw requests per second. But for a compliance SaaS, the real bottlenecks are database queries (50–200ms) and external API calls (100–500ms) — the framework itself contributes only 1–3ms per request. NestJS + Fastify can serve thousands of concurrent users on a single small server. Raw RPS is not our constraint.
Security note: using TypeScript on both frontend and backend does not double the attack surface. TypeScript is build-time only — nothing of it runs in production. The real shared-risk surfaces (npm ecosystem, V8 engine, supply chain) exist regardless of language choice. Standard mitigations (lock files, Dependabot/Snyk, SBOM, CSP headers, httpOnly cookies) cover everything.
Field signal: Vanta, Drata, Sprinto, Snyk all run Node.js/TypeScript backends. Wiz uses Go (millions of cloud assets per scan — different problem). Hyperproof runs Java/.NET (legacy enterprise). Rails is a viable minority (Secureframe).
3. Schemas & Validation — ✅ Locked
Decision: Zod as the single source of truth for every data shape that crosses a boundary — HTTP request bodies, GraphQL inputs, REST payloads, webhook bodies, BullMQ job payloads, and React forms. NestJS receives Zod schemas via a small custom ZodValidationPipe; the frontend uses the same schemas through React Hook Form's Zod resolver.
Why we chose this:
- One rulebook across the stack — a schema like
CreateVendorSchemais defined once and used by the React form, the NestJS controller, the BullMQ worker, and the AI worker. Backend and frontend rules cannot drift, because there is literally only one rulebook. This matters for S.E.T because the admin portal AI generates content (questionnaires, policies, findings) that the customer portal consumes — same shapes, opposite sides of the wire. - TypeScript types derived from schemas —
z.infer<typeof Schema>produces the matching TS type automatically. Change the schema → the type updates → every file using the type re-typechecks. No parallel "declare the class + declare the validator" pattern that drifts when one is touched and the other isn't. - Runtime validation at every external boundary —
Schema.parse(input)rejects type confusion, oversized payloads, prototype pollution, and unknown extra keys (.strict()mode) along with the functional rules (min,max,email,enum). One mechanism, one consistent error shape; security and functional concerns covered together. - Ecosystem fit — React Hook Form's Zod resolver is the modern default for form validation; TanStack Query is commonly paired with Zod to validate API responses at runtime; tRPC and most modern API toolchains are Zod-native. The libraries we're already planning to use are Zod-native.
- Standardizes input validation across the SSDLC — closes the "scanners are not a written standard" gap that ISO/IEC 27001:2022 A.8.28 and NIST SSDF PW.5 require — the Secure Coding Standard §3.1 mandates Zod at every BFF boundary.
NestJS integration: Zod is not NestJS's "canonical" validation library (that role belongs to class-validator). We pay a one-time cost of ~30 lines of glue (a custom ZodValidationPipe) to wire Zod schemas into NestJS's request-handling lifecycle. After that, every controller uses @Body(new ZodPipe(MySchema)) dto: MyDto — same ergonomics as class-validator DTOs, but the schemas now compose with the frontend and worker code.
Alternatives considered and rejected:
- class-validator + class-transformer — NestJS's canonical decorator-driven DTO library. Rejected because it cannot leave the backend: the same rules would have to be rewritten on the frontend (Yup, hand-rolled React Hook Form rules, etc.), and the two rulebooks would drift in production. Closes A.8.28 functionally but at a recurring duplication cost.
- Yup — older, Zod's predecessor in popularity. Weaker TypeScript inference; ecosystem momentum has clearly shifted away since 2024.
- Joi — Hapi-ecosystem default; loses to Zod on TypeScript inference and end-to-end story.
- Valibot — newer Zod-alike with smaller bundle. Promising but the ecosystem is too thin in 2026 to justify going off the well-trodden path.
- No validation library (manual
if (typeof x !== 'string')checks) — guaranteed to drift, miss edge cases, and fail an ISO A.8.28 audit.
Field signal: Zod crossed class-validator in npm weekly downloads in late 2024 and continues to lead through 2026. It is the de facto choice for new TypeScript-stack SaaS in 2025–2026. Compliance SaaS rarely disclose validation libraries, but Zod's adoption across the modern TS ecosystem (React Hook Form, TanStack Query, tRPC, Hono, modern OpenAPI tooling) is unambiguous.
4. State Management — ✅ Locked
Decision: TanStack Query for server state + Zustand for local UI state. Server data is held only in memory, with aggressive eviction for sensitive data and a single chokepoint that enforces "no disk persistence" and "wipe-on-logout."
Why we chose this:
- Two purpose-built tools for two different problems — data from the backend ("server state") and purely-in-browser UI flags ("local state") have nothing technically in common. Conflating them (the Redux pattern) costs boilerplate without benefit. The 2026 default is to handle them with two purpose-built tools.
- TanStack Query (server state) — fetches, caches, retries, dedupes, and refetches stale data. Pairs natively with Zod so every server response is validated at the seam. Industry-standard since ~2023 (Linear, Vercel, Mercury, Vanta).
- Zustand (local UI state) — ~1 KB minimalist store; declare a state object, use it via a hook, no providers / reducers / actions. Right-sized for "is this modal open?" or "what's the current sort order?"
- Tight security posture — minimal frontend data retention is a customer requirement. TanStack Query is configured with:
gcTime: 0(instant eviction) for sensitive queries — findings, vendor data, audit logs, evidence content, customer PII, anything tenant-scoped beyond IDs.- Default
gcTime(5 min) acceptable for non-sensitive queries — config, framework lists, UI lookups. queryClient.clear()on logout, token-refresh failure, and tenant switch — single-line wipe of all cached server data.- No persister installed. Disk persistence (
localStorage,IndexedDB, service-worker cache) is forbidden — JS heap only. - Documented and enforced in Secure Coding Standard §5.
Note on the unavoidable: any browser SPA holds data in JS memory while it is being rendered — this is true of every dashboard ever built and cannot be avoided architecturally. What this layer controls is how long that data lingers in memory after the user navigates away. With the configuration above, sensitive data is evicted within a single tick of the React lifecycle.
Alternatives considered and rejected:
- Redux Toolkit — heavier (~50 KB), substantial boilerplate (slices, actions, reducers, providers). Famous, but mostly a 2018 default. Modern teams choose TanStack Query + Zustand instead.
- Jotai / Recoil — atomic state libraries; smaller mindshare; no clear win for our profile.
- React Context + useState alone — fine for prototypes; breaks down at S.E.T's complexity (many list views, filters, pagination, cross-component state).
- No caching library (raw
fetcheverywhere) — does not eliminate data-in-memory while displayed (impossible in any SPA), but produces worse UX (loading spinners on every navigation), more backend load, and a worse security posture: no central chokepoint for "wipe on logout" or "ban disk persistence."
Field signal: TanStack Query has crossed Redux in npm weekly downloads multiple times since 2024 and is the de facto choice for new React SaaS in 2025–2026. Zustand has become the dominant minimalist alternative to Redux for local state, used by tens of thousands of production apps.
5. Database — ✅ Locked
Decision: PostgreSQL 16 as the primary store.
Why we chose this:
- ACID guarantees — compliance and audit data must be transactional and consistent. Auditors don't accept "eventual" anything.
- Row-Level Security (RLS) — Postgres enforces multi-tenant isolation at the database layer; defense-in-depth even if application code has a bug. The Architecture section commits to this pattern.
- Native table partitioning — high-volume tables (
scan_findings,audit_logs,evidence_artifacts) partitioned bytenant_id. Postgres handles partition pruning transparently. - JSONB — evidence artifacts have flexible shapes; JSONB stores semi-structured data inside a relational table.
- pgvector extension — if AI Assistant grows into RAG/embeddings, vectors live in Postgres — no separate vector database tier.
- Built-in full-text search — document/evidence search without a separate Elasticsearch cluster.
- Best ORM support — Prisma and Drizzle (both candidates for Item 6) are first-class on Postgres.
- No lock-in — open-source, available managed on every cloud (RDS, Cloud SQL, Azure Database, Supabase, Neon) and trivially in local Docker.
Alternatives considered and rejected:
- MongoDB — Vanta's 2018 choice. Document DB with weaker transactions, no relational JOINs, no foreign keys, no native RLS. Research called this "the cautionary tale."
- MySQL — fine, but PG has more advanced JSON, FTS, partitioning, and RLS.
- CockroachDB — distributed Postgres-compatible; great for global multi-region but 2–3x cost and overkill at startup stage. Migration path stays open (same SQL = same code).
- SQLite / MariaDB / TiDB / Yugabyte — none compelling for a fresh 2026 compliance SaaS.
Field signal: Drata, Sprinto, Wiz (primary store), Secureframe (inferred), Snyk (inferred) — all Postgres. Vanta is the only outlier (MongoDB, legacy decision).
6. ORM / Data Layer — ✅ Locked
Decision: Drizzle ORM with drizzle-kit for migrations.
Why we chose this:
- Schema-per-tenant compatibility — Item 6 was decided alongside the Task 5 pre-lock that S.E.T uses schema-per-tenant isolation (see Multi-Tenancy section). Prisma has long-running unresolved pain here (GitHub #12420, still open in 2026) — dynamic schema switching requires
search_pathmiddleware or per-tenant client instances. Drizzle handles it cleanly because it's a thin SQL-first builder: you control the connection andSET search_pathper request/transaction. - Data shape fit — S.E.T's data profile (deep relations: findings ↔ evidence ↔ controls ↔ frameworks ↔ questionnaires ↔ assets; multi-source ingestion: scanners, agents, API; unified aggregations) pushes Prisma into
$queryRawfor window functions, CTEs, recursive queries (BCP dependency graphs), and lateral joins — losing its DX advantage. Drizzle stays SQL-native end-to-end. - PostgreSQL feature surface — lateral joins, CTEs, materialized views, JSONB ops, and RLS primitives (
pgPolicy,.enableRLS(),crudPolicy) are first-class.WITH RECURSIVEand window functions go through Drizzle's typedsql\`` template (still type-safe). - Lightweight runtime — pure TypeScript, ~7KB min+gzip, no native binaries to ship in containers.
- Team familiarity —
set-test-claudealready uses Drizzle. Existing skill carries over. - 2026 community momentum — Drizzle crossed Prisma in weekly npm downloads in late 2025 (~5.1M vs Prisma's ~2.5M as of Q1 2026). The "newer / smaller ecosystem" concern has faded.
Honest tradeoffs:
- More verbose for simple CRUD than Prisma's
findMany({ include: {...} })API — real but minor. - Migration tooling (
drizzle-kit) is solid but less polished thanprisma migratefor complex renames — usestrict: trueto avoid rename-as-drop+add data loss. WITH RECURSIVEand window functions go throughsql\`` template rather than a pure builder (still type-safe).
Note on retracted claims: an earlier rationale draft cited a Rust query-engine memory cost for Prisma and a "Drata uses Prisma" field signal. Prisma 7 (Nov 2025) removed the Rust binary and the Drata claim could not be primary-sourced — both retracted.
Field signal: ORM is rarely publicly disclosed by compliance SaaS. 2026 community consensus (Reddit r/node, HN, t3-app selections, multiple Drizzle-vs-Prisma 2026 writeups): Drizzle preferred for SQL control, advanced features, and edge/serverless; Prisma preferred for pure DX, migration polish, and team onboarding. For S.E.T's profile + locked schema-per-tenant choice, Drizzle is the clear fit.
7. Authentication — ✅ Locked
Decision: WorkOS (with AuthKit for the hosted login UI). Default region: EU (Ireland).
Why we chose this:
- Designed for the exact problem S.E.T solves — multi-tenant B2B SaaS selling to enterprise security buyers. WorkOS's pricing and product surface align with that go-to-market shape.
- Enterprise SSO / SCIM are first-class, included — SAML 2.0, OIDC, SCIM 2.0 provisioning, MFA, audit logs are core capabilities, not enterprise-tier upsells. Every enterprise prospect will demand these on day one.
- Pricing aligns with revenue, not user count — free up to 1M monthly active users; ~$125 per enterprise SSO connection. Auth0 by comparison locks SAML/SSO behind ~$1,500+/month enterprise plans.
- Native multi-tenant model — WorkOS Organizations map cleanly 1:1 to our locked schema-per-tenant isolation: each WorkOS Organization corresponds to one Postgres schema in our DB.
- Per-tenant branding — AuthKit's hosted login displays each customer's logo and SSO method with their domain claim — required for enterprise sales.
- You don't operate identity infrastructure — one fewer attack surface for SOC 2 auditors to probe deeply; we point at WorkOS's own SOC 2 Type II report.
- Default EU residency — Israel has an EU adequacy decision, so EU region covers the majority of Israeli customers (non-government, non-Nimbus) under Amendment 13 cross-border rules.
Compliance considerations — known tradeoffs being accepted:
S.E.T's customer base includes Israeli regulated industries subject to Amendment 13 of the Israeli Privacy Protection Law (effective August 2025) and potential Nimbus Tender alignment. The tightest of these customers (Israeli government, defense, regulated healthcare, banking on Nimbus contracts) require Israeli data residency and Israeli operational control over auth data — which WorkOS (US-headquartered, US + EU regions only, no Israel region) cannot provide today.
Mitigations and Phase 2 path:
- Sub-processor disclosure in every customer DPA, with SCCs for any customer where EU adequacy alone is insufficient
- EU as default region — keeps cross-border concerns minimal for Israeli + EU + global customers
- Phase 2 — Keycloak parallel for Nimbus-aligned customers: when the first Nimbus-required customer materializes, add a self-hosted Keycloak instance in AWS Israel (Tel Aviv) alongside WorkOS. Per-tenant routing in NestJS auth middleware determines which provider handles each login based on the customer's compliance profile.
- Escape paths open — OIDC/SAML are portable standards; full migration to Keycloak (or another managed provider) remains possible if WorkOS economics or compliance constraints become untenable at scale.
Alternatives considered and rejected:
- Auth0 (Okta) — broadest ecosystem, but SSO/SCIM lives behind the Enterprise plan ($1,500+/month minimum). Bad pricing math for early stage. Same residency limitations as WorkOS, no upside.
- Keycloak (self-hosted from day one) — what
set-test-claudeuses. Zero per-connection cost, full residency control, but you own auth uptime, CVE response, SSO connection setup. Real ops burden. Deferred to Phase 2 (Nimbus customers) rather than launch-day. - Clerk — excellent DX but B2C-leaning; weaker enterprise SSO/SCIM depth than WorkOS.
- AWS Cognito / Azure AD B2C / Stytch / Supabase Auth — none purpose-built for B2B enterprise SSO at WorkOS's depth.
Honest tradeoffs:
- Per-connection cost: ~$125 per enterprise SSO connection. At 1,000 enterprise tenants, ~$125k/year. Accepted in exchange for launch-day ergonomics and not running auth infrastructure ourselves.
- No Nimbus path without Phase 2 Keycloak alongside.
- US-based sub-processor must be disclosed in DPAs; some Israeli regulated buyers will require SCCs or refuse outright (those become Phase 2 candidates).
Field signal: auth providers are not publicly disclosed by any compliance SaaS researched. 2026 momentum signal: WorkOS is what new enterprise-B2B SaaS reach for when SSO/SCIM are launch-day requirements. Keycloak remains the preferred alternative for Israeli-residency-mandated workloads (covered by our Phase 2 plan).
8. Hosting / Compute — ✅ Locked
Decision: AWS with ECS Fargate (Multi-AZ), starting in EU region (Ireland or Frankfurt). AWS Israel (Tel Aviv, il-central-1) added in Phase 2 for Nimbus-aligned Israeli customers.
Why we chose this:
- AWS — most enterprise-credible cloud for SOC 2 / Amendment 13 audits; deepest service catalog; Tel Aviv region exists specifically for Nimbus alignment (matches Item 7's Phase 2 plan).
- ECS Fargate over EKS — at S.E.T's scale (~5–10 containers per region) Kubernetes is premature engineering. Research consensus is unambiguous: teams adopting K8s too early burn 20–30% of engineering time on cluster ops. Fargate gives serverless containers, no cluster management, AWS-managed control plane.
- ECS Fargate over EC2 (Vanta's pattern) — EC2 means managing AMIs, patching, scaling. Not where the team should spend time at this stage.
- Multi-AZ for HA — Fargate tasks distributed across 3 AZs per region; ALB and RDS both Multi-AZ; no single AZ failure causes downtime.
- Three Fargate task families for resource isolation:
api— NestJS BFF, scales on request countapi-workers— notifications, reports, exports, audit log, webhooks, tenant lifecycle, maintenancescanner-workers— security scans, evidence ingestion, agent ingestion, compliance computation (higher CPU/memory; scales on queue depth)ai-workers(Phase 2) — AI Assistant, embedding generation, RAG over evidence corpus
Alternatives considered and rejected:
- EKS (Kubernetes) — premature at this scale; massive operational tax for ~10 containers.
- EC2 + NGINX (Vanta's setup) — full control but high ops burden; wrong tradeoff.
- AWS App Runner — too limited (no sidecars, restricted networking).
- AWS Lambda — wrong shape for an always-on NestJS BFF (cold-start tax, 15-min limit, awkward state).
- GCP / Azure — viable in absolute terms but team is AWS-shaped; AWS Tel Aviv exists for Nimbus, no equivalent on GCP/Azure today.
- Local Docker (set-test-claude pattern) — that codebase is local-only for different reasons; S.E.T needs production cloud.
Field signal: AWS is the modal compliance-SaaS cloud (Vanta, Snyk on AWS; Secureframe + Thoropass inferred). GCP (Drata), Azure (Hyperproof) are minority but viable. 2026 consensus for startup-scale SaaS: ECS Fargate first, EKS only when forced.
9. CDN / WAF — ✅ Locked
Decision: Cloudflare as the single edge layer (WAF, DDoS, rate limiting, CDN, DNS). The AWS ALB security group restricts inbound traffic to only Cloudflare IP ranges, and Cloudflare Authenticated Origin Pulls (mTLS) verify origin authenticity cryptographically. AWS Shield Standard (free, automatic) covers any residual AWS-edge DDoS. AWS WAF is not used.
Why we chose this:
- Cloudflare WAF — OWASP Core Ruleset + custom rules for API protection
- DDoS at the global edge — L3/L4/L7 mitigation absorbed by Cloudflare's 300+ PoPs before traffic ever touches AWS (vs scaling AWS to absorb attacks, which is expensive)
- Rate limiting per-IP + per-API-key — applied at the edge
- Bot management — defends against credential stuffing, scraping
- Cloudflare DNS with proxy — origin IP never exposed publicly
- TLS 1.2+ Full Strict — end-to-end encryption
- Edge caching — static SPA assets cached at 300+ PoPs (instant first paint globally)
- Inbound-only-from-Cloudflare at ALB security group — direct-to-AWS bypass attacks fail at the network layer
- mTLS Authenticated Origin Pulls — even if Cloudflare's IP ranges shift, only cryptographically authentic Cloudflare traffic passes the ALB
- AWS Shield Standard — free, automatic; covers residual L3/L4 DDoS at the AWS edge
Why AWS WAF is intentionally NOT used:
For an attack to reach AWS WAF, it would have to (a) originate from a Cloudflare IP, (b) carry a valid Cloudflare mTLS certificate, and (c) be something Cloudflare's WAF missed. That intersection is vanishingly small. AWS WAF would essentially see only legitimate Cloudflare-cleared traffic, while adding cost (~$5/rule + ~$1/M requests), a second WAF config to maintain, and doubled false-positive triage. Defense-in-depth has diminishing returns — at this point further layering is friction without meaningful protection.
When AWS WAF would be added later:
- If endpoints are ever exposed that bypass Cloudflare (e.g., direct AWS API Gateway for partner integrations)
- If a SOC 2 / customer auditor explicitly demands a second WAF layer
- (AWS-specific threats like compromised IAM creds calling AWS APIs are GuardDuty's job, not WAF's)
AWS Shield Advanced is not used either — ~$3k/month minimum and unnecessary because Cloudflare absorbs DDoS at the global edge before it reaches AWS.
Alternatives considered and rejected:
- CloudFront only (AWS-native) — simpler vendor count but Cloudflare has stronger DDoS at the edge and lower latency to global users
- Fastly / Akamai — premium, expensive; overkill at startup stage
- AWS WAF only, no Cloudflare — exposes origin IP, absorbs DDoS at AWS layer (expensive), slower at the edge
- Cloudflare + AWS WAF (defense in depth) — initially proposed, then rejected: the IP allowlist + mTLS makes AWS WAF redundant; not worth the cost and maintenance burden
Field signal: Vanta runs Cloudflare + CloudFront + Fastly together (mature multi-layer stack — they have the scale to justify it). Cloudflare alone is the modal startup choice.
10. Source Control — ✅ Locked
Decision: GitHub (monorepo), owned by the S.E.T organization.
Why we chose this:
- Universal field default — every compliance/security SaaS researched uses GitHub.
- The third-party dev firm accesses it as Write-only collaborators (no Admin); the S.E.T org controls all settings.
- All access + supply-chain controls are configured at the org/repo level: branch protection, CODEOWNERS, Push Protection, Allowed Actions allowlist, mandatory hardware 2FA, monthly audit-log review, quarterly collaborator reconciliation.
- Native integration with GitHub Actions (Item 11) and AWS OIDC.
Full controls: see the Supply Chain & CI/CD Security section.
Field signal: GitHub is the universal default across the field.
11. CI/CD — ✅ Locked
Decision: GitHub Actions.
Why we chose this:
- Native to GitHub (single platform, no extra vendor) — the de facto standard for new builds.
- Deploys to AWS via OIDC — no long-lived credentials stored anywhere.
- Hardened per the Supply Chain & CI/CD Security section: all third-party Actions SHA-pinned, least-privilege
permissions:per workflow, Harden-Runner egress control, job separation (untrusted code never shares a job with credentials), least-privilege deploy role, GitHub-hosted runners only, required-reviewer production deploy gate. - Two-tier deploy: auto to nonprod on merge to
develop; gated, approved deploy to prod on merge tomain.
Full pipeline + tool stack: see the Supply Chain & CI/CD Security section.
Field signal: GitHub Actions is the de facto CI/CD standard for new builds; no researched competitor contradicts it.