---
name: bt-architecture
description: Choose tenants, applications, keys, permissions, and primitive boundaries.
license: MIT
metadata:
  tier: core
  last_verified: '2026-09-25'
---

# Architecture decisions

This skill answers the questions that are expensive to reverse. Confidence discipline matters here more than anywhere: canonical facts get linked; everything else is labeled an implementation pattern; hard-to-reverse calls get an explicit "validate with Basis Theory before locking in."

## Tenants, tenant merchants, or shared? The decision tree

Canonical basis: tenants are the isolation boundary for applications and tokens (https://developers.basistheory.com/docs/api/tenants/); tenant merchants are a lighter primitive for multiple business units, regions, or sub-merchants needing independent service configuration inside one tenant (https://developers.basistheory.com/docs/concepts/what-are-tenant-merchants, API: https://developers.basistheory.com/docs/api/tenants/tenant-merchants); test tenants are a distinct environment contract (https://developers.basistheory.com/docs/api/test-tenants).

Walk the questions in order:

1. **Need hard isolation of data, access, or environment behavior?** → separate tenants. Triggers: distinct SDLC environments; separate legal entities that must not share operator access; each customer's applications may only reach their own data; rollout/quota risk isolation.
2. **Mainly service-configuration variance inside one operating domain?** → tenant merchants before splitting. Good fit: one owner/operator model, shared vault and governance, sub-merchants needing different configs, no need for full admin isolation. Fact (verified 2026-09-25): merchant context is now documented — the `BT-MERCHANT-ID` header sets the acting merchant (access scoping, rate-limit bucket, defaults), `owner_merchant_id` sets ownership of new tokens and Network Tokens, `configuration_merchant_id` selects provider configuration, and `merchant_id` is a deprecated configuration-only alias. With the header, only that merchant's resources are visible and there is no retroactive association of pre-existing tokens; without it, calls run at tenant level. Reactors and Proxy transforms never auto-forward the header (https://developers.basistheory.com/docs/concepts/what-are-tenant-merchants#merchant-context-acting-as-a-merchant). **Hedge:** validate the ownership-vs-configuration partition with Basis Theory before locking in, because ownership cannot be applied to tokens after the fact.
3. **Must the same card/credential be reused across sub-clients or merchants?** → tenant count should usually go _down_, not up. This is exactly where one-tenant-per-client advice breaks: cross-tenant reuse creates duplication, lifecycle complexity, and painful dedupe/reporting. Map the credential-sharing rules first; prefer the fewest tenants that hold the required trust boundary.
4. **Do downstream operators/admins need independent control planes?** → lean separate tenants; operator-boundary mistakes become permanent operational burden, not day-one setup cost.
5. **Just separating test from prod?** → that's the environment split, not a topology question. Test tenants differ materially: `api.test.basistheory.com`, different outbound IPs, `v1-test` webhook signatures, and no PCI compliance for live data (all Fact: test-tenants page + https://developers.basistheory.com/docs/api/ip-addresses).

Anti-patterns to name when you see them: tenant-per-client before mapping trust boundaries; using tenant count to resolve unclear ownership (an org problem wearing an architecture costume); treating test tenants as production-with-a-flag.

Escalate (recommend talking to Basis Theory) when: legal-entity or compliance boundaries drive the design; a credential must span merchants; tenant merchants would carry the design but docs are too thin to commit safely; or the choice locks in a hard-to-reverse partition. Platform merchant onboarding flows: https://developers.basistheory.com/docs/guides/merchants/onboarding.

## Applications, keys, and access rules

Facts: application types and their key semantics — https://developers.basistheory.com/docs/api/applications/, authentication and key usage — https://developers.basistheory.com/docs/api/authentication, frontend client keys — https://developers.basistheory.com/docs/api/client-keys, permissions — https://developers.basistheory.com/docs/api/applications/permissions, access rules with container scoping — https://developers.basistheory.com/docs/api/applications/access-rules, containers — https://developers.basistheory.com/docs/concepts/what-are-containers, access-control model — https://developers.basistheory.com/docs/concepts/access-controls, sessions for short-lived elevated access — https://developers.basistheory.com/docs/api/applications/sessions, https://developers.basistheory.com/docs/guides/govern/sessions.

Implementation-pattern defaults: one application per workload, not per developer; public applications in frontends, private keys only server-side; scope by container path so adding data classes later doesn't mean re-cutting every key; sessions (not long-lived broad keys) for reveal flows; when access/auth friction appears, it is usually a scope/key/application mapping error — check that before assuming product limitation. Terraform provider for topology as code: https://developers.basistheory.com/docs/sdks/server-side/terraform.

Two documented high-risk grants (Fact, verified 2026-09-25): Management keys holding `application:create` or `application:update` are administrative credentials — give them only to the automation that provisions Applications and rotate on exposure (https://developers.basistheory.com/docs/api/applications/permissions); and `token:update` on a Public Application lets an exposed browser key modify any token whose ID is known, so prefer a backend Private key, Sessions, or Token Intents (https://developers.basistheory.com/docs/concepts/access-controls). `token:reveal` cannot be granted to an Application at all; it exists only as a runtime permission for Node.js reactor and transform images (`bt-reactors`).

## Proxy vs reactor vs direct — the primitive rubric

- **Proxy default**: use Proxy whenever the job is secure synchronous forwarding, detokenization, request/response transforms, or preserving an existing HTTP call shape. This should cover most card-to-processor and sensitive-data forwarding designs. (`bt-proxy`)
- **Reactor breakout**: use a Reactor only when Proxy cannot safely express the workflow: genuine programmable plaintext logic, branching, multi-step orchestration, side effects, async/background work, or formats that do not fit transforms. (`bt-reactors`)
- **Direct**: use Direct only when no sensitive data crosses the boundary and Basis Theory in the path adds no security, observability, or migration benefit.

Smell tests (implementation patterns): making async webhook/reactor machinery "fast enough" for checkout = wrong primitive, not tuning problem. Reactor that only makes one HTTP call = should be the proxy. Choosing a reactor before checking Proxy means the recommendation is probably overbuilt.

## Output contract

This skill produces: the decision, the tradeoff that decides it, a topology sketch (tenants/apps/keys per environment), the assumptions list, and the reversibility note (what this choice forecloses). For platform customers, the collection surface — Elements iframes vs own-form client-side encryption vs inbound capture (`bt-collect`) — is a first-order architecture decision; decide it before tenant topology detail. Implementation then routes to the feature skills. For an environment build-out order: tenants → applications/keys → access rules → test-tenant wiring → parity checklist (`bt-production`).

## Basis Theory implementation guidance

When the answer needs implementation judgment beyond public docs, load only the relevant slices below from [../\_shared/references/implementation-guidance/README.md](../_shared/references/implementation-guidance/README.md). Treat these as implementation patterns, not canonical product behavior; keep public docs as the source of truth and label the guidance as **Implementation pattern** or **Assumption** unless docs prove it.

- `../_shared/references/implementation-guidance/platform-identity.md`
- `../_shared/references/implementation-guidance/concept-routing.md`
- `../_shared/references/implementation-guidance/tenants-and-access/`
- `../_shared/references/implementation-guidance/vsaas-payfac-platforms/`
- `../_shared/references/implementation-guidance/resilience/`

<!-- bt-conventions:start -->

## Working conventions (shared by all Basis Theory skills)

- Public Basis Theory docs are the source of truth for product behavior. Link the specific docs page behind any product claim; if a skill and the docs disagree, follow the docs.
- Keep customer ownership clear: customers own PSP accounts, credentials, compliance decisions, production rollout, and application code. Skills can guide plans, bounded changes, and examples; they do not certify production readiness.
- Never request, echo, store, or commit real credentials. Use placeholders such as `<BT_API_KEY>` and prefer sandbox or synthetic data for examples.
- If a live-looking credential, live cardholder data, or production secret appears, stop mutation work, tell the user exactly where it appeared, and require rotation/revocation before continuing.
- Default to test tenants and PSP sandboxes. Do not run or recommend production charges, refunds, card reveals, credential migrations, or data moves unless the user explicitly asks and the plan names safeguards, rollback, and customer approval.
- Treat vague planning, audit, review, and “should we” prompts as read-only unless the user explicitly asks for a bounded implementation change.
- Before changing product code, map the existing checkout/data flow, credential-bearing paths, persistence, retries, refunds, webhooks, logs, and rollback expectations.
- End with an honest final report: done, assumed, remaining, proof run, unverified live behavior, rollback, and go-live items.
<!-- bt-conventions:end -->
