SDK Architecture Reference

The Folder Structure
That Scales to 5,000+ Partners

Every folder has one job. Every partner is config, not code. Your business logic stays 100% hidden behind the proxy layer.

SOC2 Type II IPO Ready 5,000+ Partners Cloudflare Protected TypeScript
🔐 How Everything Connects
🌐
Partner Website
stripe.com, razorpay.com, any partner site
PARTNER SEES
📦
embed.js
sdk.revenued.com/v1/embed.js — the only file partners ever get
PARTNER SEES
🛣️
/v1/sdk/* routes
submit · config · health · fields · flows
PARTNER TOUCHES
🔀
src/proxy/
authenticates → encrypts SSN/EIN → maps payload → strips internals
INVISIBLE
⚙️
revenued-middleware
your real business logic — createSubmission, all internal processing
100% HIDDEN
🤖
revenued-mcp-server
AI tooling and orchestration layer
100% HIDDEN
Golden Rule: Partners only ever see embed.js and a simplified API response.
Everything else — your tech stack, middleware contracts, internal structure — is completely invisible.

Clean. Purposeful. Scalable.

Every folder has one job. You always know where to look.

revenued-sdk/ — project root
revenued-sdk/
├── src/                  ← all your server-side code
│ ├── auth/              🔑 Is this partner allowed in?
│ ├── config/            ⚙️ What are this partner's rules?
│ ├── routes/            🛣️ All API endpoints, versioned
│ │ └── v1/           submit · config · health · fields · flows
│ ├── middleware/       🚦 Runs on every single request
│ ├── proxy/            🔀 Internal bridge to revenued-middleware
│ ├── mapper/            🗺️ Transforms payloads in and out
│ ├── encryption/       🔒 RSA encrypt SSN, EIN
│ ├── schema/            📋 Every field, rule, and default
│ ├── cache/             ⚡ Redis — what makes 5K partners possible
│ ├── events/            📡 Outbound webhooks to partners
│ ├── embed/            📦 The embed.js client SDK
│ ├── widget/            🖼️ Form UI — views, headless, themes
│ ├── health/            🩺 Is the SDK alive?
│ └── logger/            📝 Logs everything — no PII ever
├── partners/           👥 One folder per partner
│ ├── _template/       ← copy this for every new partner
│ ├── stripe/
│ └── razorpay/
├── db/                  🗄️ DB migrations for sdk_partners table
├── tests/               🧪 unit · integration · performance · browser
├── docs/                📚 Field reference, SOC2, IPO, ADRs
├── infra/               🛡️ Cloudflare WAF + Terraform UAT/PROD
└── scripts/            🤖 Automate onboarding and key rotation
green = added for 5,000-partner scale (cache, events, db, scripts)

One Folder. One Job.

When something breaks at 2am, you know exactly where to look.

🔑
src/auth/
Authentication
Checks every request before anything else runs. Validates x-sdk-key, resolves the partner, and maps their public key to the internal token — silently.
api-key.service.tsgenerates pk_live_* / pk_test_* / sk_*
sdk-auth.middleware.tsvalidates key → resolves partner
token-resolver.tspk_live_* → internal token (never exposed)
key-rotation.tsrotate keys without downtime
Why: Partners never touch your middleware. Their key only knows about the SDK. The real internal token is resolved server-side, invisibly.
⚙️
src/config/
Partner Config
Loads and caches every partner's configuration. Knows their allowed fields, CORS origins, and whether it's UAT or PROD — without hitting the DB on every request.
partner.config.tsloads config by API key
partner.cache.tsRedis cache — avoids DB on every req
env.tsvalidates all env vars (Zod). Fails fast.
env.resolver.tsauto-switches UAT vs PROD config
Why: At 5,000 partners you cannot hit the DB on every request. Cache means config is loaded once and served fast every time.
🛣️
src/routes/v1/
API Routes
All API endpoints, organized by version. Adding v2 never breaks partners on v1. The /v1/ folder structure is the versioning strategy.
submit.route.tsPOST /v1/sdk/submit
config.route.tsGET /v1/sdk/config/:token
health.route.tsGET /v1/sdk/health
fields.route.tsGET /v1/sdk/fields/:partner
flows.route.tsGET /v1/sdk/flows/:partner
Why: Breaking changes? Add v2/ folder. Old partners keep working on v1. No scrambling. No outages.
🚦
src/middleware/
Request Guards
A chain of checks that runs on every single request, before any route logic fires. Your security perimeter inside the server — second line of defence after Cloudflare WAF.
cors.middleware.tsonly allows origins in partner's origins.yaml
rate-limit.middleware.tsper-partner limits backed by Redis
request-id.middleware.tsstamps each request with trace ID
audit-log.middleware.tsimmutable log for SOC2 CC7
waf-hints.middleware.tsreads cf-ray, cf-ip from Cloudflare
Why: Even if WAF lets something through, this is the second wall. The audit log here is what satisfies SOC2.
🔀
src/proxy/
The Internal Bridge
The most important folder. Internal bridge between the SDK and revenued-middleware. Partners never know it exists. Does three things: authenticate, transform, forward.
submit.proxy.tsauth → encrypt → map → call → strip
middleware-client.tsHTTP client to revenued-middleware
mcp-client.tsHTTP client to revenued-mcp-server
response-filter.tsstrips ALL internal fields before response
Why: response-filter.ts is the last line — nothing internal ever leaks out to a partner.
🗺️
src/mapper/
Payload Transformer
Translates between the simple world partners see and the complex internal world revenued-middleware expects. Lives separately from proxy because mappers are independently testable.
flat-to-modal.mapper.tsflat SDK payload → merchant_info_modal
modal-to-flat.mapper.tsfull MW response → { p_id, link }
field-strip.mapper.tsremoves internal-only keys from any object
Why: Partner sends 8 clean fields. Middleware expects a deeply nested structure. The mapper is the translator — swappable without touching the proxy.
🔒
src/encryption/
RSA Encryption
Encrypts sensitive fields server-side using RSA before they reach revenued-middleware. The client SDK never holds plaintext sensitive data — encryption happens in the proxy.
rsa.service.tsRSA-OAEP encrypt/decrypt
field-encryptor.tsauto-detects ssn/ein by name pattern
key-manager.tsversioning, rotation, KMS integration
mask.tsshows ***-**-1234 in logs and responses
Why: Add a new sensitive field to the pattern list and it's automatically encrypted everywhere. No code changes needed across the codebase.
📋
src/schema/
Field Registry
Single source of truth for every field. What's mandatory. What's optional. What the defaults are. Per endpoint. Per partner. Add a field once, validated everywhere.
fields.registry.tsevery field: type, mandatory, optional, default
submit.schema.tsZod schema for POST /v1/sdk/submit
config.schema.tsZod schema for config response shape
partner-fields.tsper-partner overrides (stripe ≠ razorpay)
Why: Stripe can hide amountRequested with default 25000. Razorpay makes businessType mandatory. All config, no code changes.
src/cache/
Redis Cache Layer
Redis-backed caching for everything that shouldn't hit the database on every request. This is what makes 5,000 partners possible.
redis.client.tsRedis connection with retry + health
config.cache.tspartner config with TTL + invalidation
rate-limit.store.tssliding window counters per partner
Why it was added: Rate limiting breaks the moment you run 2 server instances without Redis. Config cache means one DB read serves millions of requests. This was missing — now it's built in from day one.
📡
src/events/
Outbound Webhooks
Sends reliable server-to-server webhooks to partners after submission completes. onComplete() in the browser isn't reliable — tabs close, users navigate away.
webhook.service.tsPOSTs to partner's webhook_url
retry.tsexponential backoff on failed webhooks
signature.tsHMAC-SHA256 signs payload so partners verify it
Why it was added: This is the reliable confirmation channel. Signature means partners can prove the webhook came from Revenued, not a spoofed request.
📦
src/embed/
Client SDK (embed.js)
The client-side JavaScript SDK. Three lines of code for the partner, a lot of engineering underneath. Handles isolation, form, events, API calls, and cross-browser compat.
core/sdk.tsRevenuedSDK.init() — the one public entry point
isolation/iframe + Shadow DOM strategies, auto-selected
form/state machine driven by partner config
compat/polyfills, browser detect, React Native hints
Why: isolation/ prevents CSS conflicts. feature-flags.ts lets you roll out to 1 partner before all 5,000.
🖼️
src/widget/
UI Component System
The actual form UI. Built two ways: full Revenued-styled UI out of the box, or headless for partners who want complete design control over every pixel.
views/BusinessInfo · OwnerInfo · Review · Success
headless/all logic, zero UI — partner brings own design
components/Input, Select, MaskedInput (SSN/EIN)
themes/design tokens + partner color/font override
Why: Stripe wants it to look like Stripe. Razorpay wants their design. Headless gives partners 100% UI control while Revenued keeps 100% logic control.
🩺
src/health/
Health Check
A health endpoint that tells you whether the SDK is actually working — not just running. Checks DB, Redis, middleware ping, and encryption key status on every call.
health.service.tschecks DB, Redis, middleware, keys
version.tsreturns SDK version, config hash, env name
Why: Kubernetes uses this to restart the pod. Monitoring uses this to alert before partners notice. The version field tells you exactly what's deployed without SSH.
📝
src/logger/
Safe Logging
Logs everything that happens, safely. No SSN. No EIN. No passwords. Ever. The audit trail is immutable — what SOC2 auditors need to see.
audit.logger.tsimmutable trail — who, what, when (SOC2 CC7)
structured.logger.tsJSON logs with trace ID on every line
redact.tsauto-strips ssn, ein, dob from any log
Why: redact.ts means a developer can't accidentally log an SSN in a debug line. SOC2 audit prep takes hours, not weeks.

Adding a Partner = Copying a Template

No code changes. No deployments. No risk of breaking other partners.

5 YAML files. That's a new partner.
⚙️
config.yaml
partner_id, portal_id, environment (UAT/PROD)
📋
fields.yaml
which fields are mandatory / optional / hidden / default. Stripe hides amountRequested with default 25000. Razorpay makes businessType required. All config.
🔄
flows.yaml
which form steps are shown and in what order
🌐
origins.yaml
allowed CORS origins per environment — only https://stripe.com can call the SDK as stripe
📡
integrations.yaml
webhook URLs, external callbacks, third-party hooks after submission
Example — partners/stripe/fields.yaml
fields.yaml
# Stripe partner field config
mandatory:
  - businessName
  - businessType
  - ein
  - ownerName
  - ssn
optional:
  - phoneNumber
hidden:
  - amountRequested
defaults:
  amountRequested: 25000
  currency: "USD"
YAML is version-controlled — you can see every change ever made to a partner's config. Every change goes through code review. No manual DB edits.
5K+
Partners
Redis cache means config is loaded once per TTL, not once per request. The DB never becomes the bottleneck.
0
Code Changes to Add a Partner
Copy _template/, fill 5 YAML files, run the onboard script. Partner #5000 takes the same time as partner #1.
30yr
Design Horizon
v1/ → v2/ versioning means old integrations never break. New versions add, never remove. Backward compatible forever.

5 Principles Behind Every Decision

These aren't rules. They're the reason this structure works at scale.

1
One Concern, One Folder
auth/ only does auth. mapper/ only maps. encryption/ only encrypts. When something breaks at 2am, you know exactly which folder to open. No hunting across 10 files trying to find where validation lives.
2
Partners Never Touch Code
Adding a partner = copying a YAML template. Changing a partner's fields = editing their fields.yaml. Zero code changes. Zero deployments. Zero risk of accidentally breaking another partner while adding a new one.
3
Scale Is Built In, Not Bolted On
cache/ was designed in from day one — not added when things got slow at partner #200. events/ was designed in from day one — not hacked in when partners complained their webhooks were unreliable. The pain of retrofitting is worse than building it right.
4
The Proxy Protects Everything
revenued-middleware and revenued-mcp-server are never exposed. The proxy is the only bridge. It strips, encrypts, and maps. response-filter.ts is the last checkpoint — nothing internal ever leaks out regardless of what the middleware returns.
5
Auditors and New Engineers Can Read It
docs/security/soc2-controls.md maps every SOC2 control to the file that satisfies it. docs/adr/ explains every major decision ever made. A new engineer on day one can understand the whole system from the docs/ folder without asking anyone.

All 27 Points. All Covered.

Every requirement maps to an exact file. No guessing where something lives.

# Requirement Where It Lives
1Partner config managementpartners/[slug]/config.yaml · src/config/
2Mask internal fieldssrc/mapper/field-strip.mapper.ts · src/proxy/response-filter.ts
3Hide tech stacksrc/proxy/response-filter.ts · WAF strips X-Powered-By
4SDK health checksrc/health/ · src/routes/v1/health.route.ts
5Unit test SDKtests/unit/ · tests/integration/
6Document verticalsdocs/sdk/field-reference.md
7Origin whitelistpartners/[slug]/origins.yaml · src/middleware/cors.middleware.ts
8Access controlssrc/auth/ · src/middleware/sdk-auth.middleware.ts
9UAT vs PROD envssrc/config/env.resolver.ts · infra/terraform/uat + prod/
10Allowed flows per partnerpartners/[slug]/flows.yaml · src/routes/v1/flows.route.ts
11RSA field encryptionsrc/encryption/rsa.service.ts · src/encryption/field-encryptor.ts
12Mandatory / optional / default fieldssrc/schema/fields.registry.ts · partners/[slug]/fields.yaml
13WAF rulesinfra/cloudflare/waf/ · infra/cloudflare/workers/
14SDK authenticationsrc/auth/api-key.service.ts · src/auth/sdk-auth.middleware.ts
15URL naming conventionssrc/routes/v1/ structure · docs/sdk/url-naming.md
16Versioning managementsrc/routes/v1/ → v2/ pattern · docs/sdk/versioning.md
17Best folder structureThis document
18IPO standardsdocs/security/ipo-checklist.md · src/logger/audit.logger.ts
19SOC2 standardsdocs/security/soc2-controls.md · src/middleware/audit-log.middleware.ts
20Business logic in middlewaresrc/proxy/middleware-client.ts — SDK maps and calls, never owns logic
21Performance / load testingtests/performance/ — k6 scripts
22Cross-browser compatibilitytests/browser/ Playwright · src/embed/compat/
23Cross-language compatibilitysrc/embed/compat/lang-compat.ts · docs/sdk/cross-language.md
24View-based SDKssrc/widget/views/
25Headless SDKssrc/widget/headless/ · docs/sdk/headless-sdk.md
26Tech stack decisiondocs/adr/001-sdk-language.md
27External integrationspartners/[slug]/integrations.yaml · src/events/
+Redis cache at scalesrc/cache/ — added for 5K partner scale
+Reliable webhookssrc/events/webhook.service.ts — added for partner reliability
+Automated onboardingscripts/partner-onboard.sh — 1 command per partner
+DB migrationsdb/migrations/ — schema changes are version-controlled