Architecture

The API framework behind your product

Build an API that your applications, integrations and AI agents call — with the CustomAPIs multi-tenant framework inside it.

The framework does what a table-to-API platform does — your database, exposed as a secure API without writing the endpoints — and then keeps going, because a product API needs more than reads and writes. All of it comes from declarations you can read rather than handlers you have to write:

All of this is provided in a Node and TypeScript library that talks to PostgreSQL. It also gives you:

Which means you keep:

  • Your application and its business rules — ordinary TypeScript in your own service, not configuration in someone else’s platform.
  • Control of your own API — you run it, on your infrastructure, against your PostgreSQL schema. Nothing sits between your callers and your API.

The framework mounts as a router inside your service, so new routes built on it run alongside the routes you already have — start with one resource.

At a glance

RuntimeA stateless, TypeScript-first Node.js service (Express)
DatabasePostgreSQL — the only hard runtime dependency
DeploymentYour infrastructure; reference stack AWS Elastic Beanstalk with RDS
AdoptionMounts alongside existing routes in the same service — no rewrite
LicenceProprietary; perpetual for the API delivered, on the terms in the Core Package
ContinuityIf the relationship ends, what’s deployed remains yours to run

One API shape, every resource

Consistency is the quiet feature. Every resource is reached the same way, answers in the same envelope, fails in the same shape, and filters and pages with the same grammar — so your frontend team, your customers’ integration engineers and any AI consumer learn the API once instead of once per endpoint. It holds because the shape is derived from the declaration rather than written per route: deviating takes effort rather than discipline.

Adding a resource is three steps:

  1. Create the table

    Your schema, your migration, your indexes. The framework reads the model you declare over it.

  2. Expose the routes

    A declaration naming the model and the operation. No handler, and no endpoint written by hand.

  3. Grant the permission

    Each operation has its own, named from the model. Until it is granted, every route is closed.

Everything else on this page is already attached at that point — the tenant constraint inside the query, the audit record inside the transaction, the validation, the events and the documentation. There is no fourth step where the safety gets added.

SEARCH   GET      /{resource}        filters and sort via query params
CREATE   POST     /{resource}        parent references in the body
GET      GET      /{resource}/{id}
UPDATE   PATCH    /{resource}/{id}   JSON Merge Patch, not replace
DELETE   DELETE   /{resource}/{id}

Paths are flat. Parent scoping is a filter or a body field, never a URL segment — there is no /events/:eventId/rounds, because the ownership constraint that would justify the nesting is already inside the query. Updates are JSON Merge Patch: the fields you send are written, the fields you omit are untouched. There is no PUT, so no caller can blank a record by forgetting a field.

{ "data": { … }, "meta": { "requestId": "…", "page": { … } } }

{ "error": { "code": "…", "message": "…", "details": [ … ], "requestId": "…" } }

Every success carries its data and a request id; meta.page appears on a search. Every failure carries a code, a message and the detail needed to correct the call — which is what lets a mistaken caller, human or agent, fix itself without a support conversation.

Reads share one query grammar: a closed operator set over the fields a caller may filter. Closed is the point — a fixed grammar can be documented completely, validated at the edge, and called correctly by an agent that has only read the documentation. Views expand related data on request, and a view can carry narrowing the caller cannot relax.

Paging is always on — not a default a caller can lift but a fixed ceiling on the rows any one request can return, which is what keeps response times consistent as tables grow and lets an endpoint behave at a million rows the way it behaved at a thousand. Totals are opt-in, because counting a large table costs a query most callers would rather not pay for. The live demo answers in exactly these shapes.

A secure pipeline for every API request

Every request flows through the same fixed pipeline. Authentication and authorisation run on every route, before validation and before any data is touched. The operation — your business logic and its write — runs inside a database transaction. Where a route demands it, the audit record is written inside that same transaction, so a committed change cannot lack its record. Events are published only after the transaction commits, to your own event infrastructure. A completion log fires on every exit path.

Nearly every stage takes before, override and after hooks — object access, the operation, response shaping and event publication among them. Validation hooks before and after, with its schema patches as the override; audit extends through its own configuration. Authenticate and authorise always run. Consistency is structural: every endpoint shares response shapes, error shapes, filtering and paging, because deviating takes effort rather than discipline.

Tenant isolation is derived, not remembered

The tenancy root is one of your own tables — an organisation, an account, a workspace. The framework has no tenant table of its own, and no opinion about which of your models it should be.

It is not a tenant_id column on every table, and not a filter every query has to remember. Each model’s path to the root is derived from the relationships you have already declared, and the constraint is part of the query that loads the data rather than a step after it. There is no per-endpoint filter to forget, because none is ever written.

Tenant context is derived from data, never trusted from the session. The request's tenant comes from the record being accessed, not from a claim the caller presents, and records cannot be moved across tenants by update.

Non-disclosure is structural. Cross-tenant access, out-of-scope access and genuinely missing records are indistinguishable: all read as not-found. Permission denied exists only for you hold this permission nowhere, decided before anything is loaded — so record existence cannot leak through error shapes.

See the demo

None of that has to be taken on trust. The live demo is this framework running a small timesheet API for two companies, with the keys published and no signup — change who is asking and watch the same URL answer differently, then read the route declaration that produced the answer.

Record and list authorisation from one scoped decision

When a route requires a permission, the framework asks one question: where may this user act? The answer is zero or more tenants: those where the user holds that permission, resolved exactly once per request. An empty answer is refused immediately, before any data is loaded. A non-empty answer becomes the tenant predicate for everything the request touches: it gates the single record on a read or write, and it constrains the list on a search.

That the answer is a set rather than a single tenant is both the safety property and the reason one call can span every tenant the caller may act in. Reach across tenants comes from authorisation, never from deployment topology.

Because one decision governs both, the detail view and the list view cannot disagree. Scoping is enforced by default in every framework query path, and opting out is explicit and visible in the route definition — a claim a code review can check. The predicate always carries the full resolved set: there is no silent truncation, and a scope too large to execute fails visibly rather than returning a partial result that looks complete. Where business logic needs its own decision, application code asks for the decision in context rather than assembling tenant filters of its own. Where code does take the resolved tenant context in hand, that is explicit and visible — the same rule as every other opt-out.

Your business rules, in ordinary code

Routes come in three tiers. Convention routes declare a model and an operation and nothing else: the full pipeline runs with derived behaviour, and a resource is five conventional routes — search, create, read, update, delete. Extended routes hook individual steps and leave the rest untouched. This is where most business rules live — a targeted change to one step, with every other guarantee intact.

Both tiers are visible in the live demo, and each link below goes to the code it describes: the timesheet route declarations carry no handler at all, and the one rule that needed writing — approving a timesheet — is fourteen lines, with the transaction, the row lock, the audit entry and the mandatory written reason still coming from the declaration beside it.

Custom routes own the flow from validation onward, but authentication and authorisation always run first: a custom route cannot accidentally skip auth, and opting out is only ever explicit.

Named business actions — approve, cancel, refund — are deliberately not a special construct. They are standard routes on action paths, retaining the transaction, the tenancy checks, authorisation and audit. There is no separate command type that escapes the guarantees, and business rules stay in ordinary application code — never in stored procedures or sidecars.

A hook runs inside the request, and receives the records it is processing — unaltered and already tenant-checked, so the common case needs no query at all — together with any changed records, so a rule comparing before and after has both in hand. It also receives the request’s own transaction: a hook’s writes commit or roll back with the operation, and the audit record riding that transaction covers them.

The framework’s own query paths are always tenant-scoped. Where a hook writes its own query, that is deliberate, visible code with the resolved tenant context in hand — something a reviewer sees, not something a filter forgot.

Every step of the pipeline is an extension point, and business logic lives in ordinary functions with direct database access where it needs it. Where a capability isn’t in the framework yet, it is usually on the roadmap rather than off the table.

Predictable API performance

The request path is engineered to a fixed budget, enforced by regression tests rather than convention:

  • A fixed, small number of database queries per request, with no N+1 patterns.
  • Exactly one authorisation resolution.
  • At most one external call on the hot path — identity resolution, behind a cache.
  • Post-commit work, such as event publishing, detached from the response path.
  • Pagination by default with a hard ceiling, so large result sets are never materialised.

An API built to scale

Because the per-request cost is fixed, growth is a capacity decision rather than a rebuild. The levers are the ordinary ones, and the framework is built so they stay available:

  • More service instances — the services are stateless with no node-local coordination, so added capacity is a count rather than a coordination problem.
  • Read replicas, used by default — where you run replicas, read routes use them without being asked, and a route that needs the primary can say so. Writes are pinned to the primary by construction, so a write path never reads from a replica.
  • A larger or better-tuned database — the schema stays yours, so indexes, configuration and physical design remain your decisions to make.
  • Storage profiles for the audit trail — from a plain table up to partitioning by tenant and month, so a long retention obligation does not weigh on the working data.

What growth does not require is moving the tenancy boundary. Because a tenant is a predicate in the data rather than a separate schema or database, adding tenants does not add deployment units, and the shape of the system at a thousand tenants is the shape it had at ten. Deployment covers how the services are run.

Tested behaviour, not promised behaviour

Delivered systems ship with a tested lifecycle: validation, permissions, data access, auditing and event publication are verified across the system, including tests that assert cross-tenant access denies. A no-database validation gate checks models and routes in CI on every change, so a contract violation fails the build, not the release.

Tenant-scoped integration keys

Tenant APIs are typically integration-heavy, so external key mapping is a built-in convention rather than an add-on. Any integrating system can attach its own identifiers to your records under its own key names, without a schema change per integrator. Keys are tenant-scoped, uniqueness is enforced per tenant and key name, and values are readable and filterable like ordinary fields. Moving an identifier between key names on a record is a single atomic update, and an opt-in lookup route resolves records by external key. Key changes are audited like any other change.

Permission-aware API documentation

Human-readable documentation, OpenAPI and an agent-oriented form are all generated from the same route registry that serves traffic, so the documented surface cannot drift from the running API. Served documentation is also permission-aware: filtered through the same authorisation decision that gates live calls, a consumer's documentation is scoped to their access. Publication is explicit — private by default, opened deliberately.

AI, LLM and agent-ready by construction

The framework treats AI agents as first-class consumers and first-class builders. Consumers get documentation an agent can trust — a projection, never a maintained description. They also get errors engineered for self-correction: every client error says what was wrong, what is allowed and, where relevant, what to do next, so a mistaken caller, human or agent, corrects itself without a support conversation. Builders close the loop against the same validation gate described under tested behaviour, and installable skills put the framework's conventions in front of a coding agent from the first prompt. Everything the agent sees derives from the same metadata that secures the runtime.

Where the tenant-aware API framework fits

The framework suits products with a particular shape:

  • Relational — your domain lives in tables and relationships, on PostgreSQL.
  • Ownership derived from your model — tenant, account, user or globally shared, the root is one of your own tables. Whose record is this? is answered by following relationships you have already declared.
  • Node.js and Express — the service stays yours; the framework is a library inside it.

And a particular set of needs:

  • Access that varies by place — who may act, and where, not just what role they hold.
  • An answer for what changed — who changed it, and why, held to a standard that survives a dispute.
  • Business rules that must hold — approvals, state transitions, validation that is more than field types.
  • Other systems that must react — business events published to your own infrastructure, only after the write commits.

Put together, these are the products where the expensive failures are cross-tenant disclosure and a change nobody can explain.

It asks one thing of your domain: your models satisfy a declared contract — the shape that lets isolation, validation and documentation be derived from them. Models are declared in the framework’s own model layer. Your PostgreSQL schema, your data and your business logic stay yours: logic lives in ordinary functions, not in a platform DSL. The contract is settled up front rather than homework you do first, and the framework validates it on every change. What you get back is that tenant data stays protected without a tenant filter anywhere in your code.

When a multi-tenant API framework is the wrong choice

Some products should look elsewhere, and it is cheaper to know that now:

  • Internal tools and prototypes — where enterprise-grade isolation, audit and permissions aren’t the point, an instant-API tool switches on faster.
  • A domain that cannot meet the contract — if ownership doesn’t run through the model relationships, the isolation this framework derives has nothing to derive from.
  • A stack you don’t want to run — the service is Node.js on PostgreSQL, deployed into your infrastructure and operated by you or by us.
  • A product life too short for the payoff — if it will never need an audit trail or a tenancy boundary you can defend, this is more architecture than the problem deserves.

Building it yourself, or starting from ours? →

How you get it

We build the API. Your developers build the screens and portals on it, and you operate the system or we do. Deep changes to the API itself come from us, or from your own developers once they are up to speed — either can be scoped in. The licence is perpetual: what we deliver stays yours to run, on the terms in the Core Package.

Where next

  • Audit — field-level diffs, the enforced human "why" and retention.
  • Identity — bring your identity provider; one cached, fail-closed seam.
  • Access Management — the control plane for many APIs sharing one authorisation model.
  • Deployment — everything in the runtime path runs in your infrastructure.