Multi-tenancy cache utilities for PostGraphile. This package implements exact-match buildKey-based handler reuse for Constructive's GraphQL server runtime.
The runtime model is intentionally conservative:
- reuse handlers only when build inputs match exactly
- no template sharing
- no SQL rewrite
- no fingerprint-based handler reuse
npm install graphile-multi-tenancy-cacheThis package is a runtime orchestrator, not a schema plugin. You configure it with a preset builder, then resolve handlers per request.
import {
configureMultiTenancyCache,
getTenantInstance,
getOrCreateTenantInstance,
flushTenantInstance,
shutdownMultiTenancyCache,
} from 'graphile-multi-tenancy-cache';
configureMultiTenancyCache({
basePresetBuilder(pool, schemas, anonRole, roleName) {
return {
extends: [],
grafast: {
context: () => ({})
},
pgServices: [],
};
},
});
async function handleGraphql(req, res) {
const svcKey = req.svc_key;
let tenant = getTenantInstance(svcKey);
if (!tenant) {
tenant = await getOrCreateTenantInstance({
svcKey,
pool: req.pgPool,
schemas: req.api.schema,
anonRole: req.api.anonRole,
roleName: req.api.roleName,
databaseId: req.api.databaseId,
});
}
tenant.handler(req, res);
}
process.on('SIGTERM', async () => {
await shutdownMultiTenancyCache();
});- Exact-match buildKey reuse — handlers are shared only when connection identity, schema set, and role inputs match exactly
- Request-key indirection —
svc_keyremains the routing and flush key whilebuildKeybecomes the handler identity - Single-flight creation — concurrent requests for the same
buildKeycoalesce onto one in-flight handler build - Safe rebinding — reassigning a
svc_keyto a newbuildKeycleans up unreachable handlers and stale indexes - Targeted flush APIs — evict by
svc_keyor bydatabaseId - Handler lifecycle management — graceful disposal and full shutdown support
- Diagnostics-friendly — exposes cache stats and
svc_key -> buildKeylookup helpers
| Concept | Meaning |
|---|---|
svc_key |
Request routing key. Used to look up which cached handler the current request should hit. |
buildKey |
Handler identity. A canonical string computed from the inputs that materially affect Graphile instance construction. |
databaseId |
Metadata/flush key. Used to evict all handlers associated with a database. |
buildKey is computed from:
- connection identity
- schema list
anonRoleroleName
It does not include:
svc_keydatabaseId- request host/domain
- auth tokens or transient headers
The value is stored as a canonical plain-text key rather than a truncated hash, so different build inputs cannot collide onto the same handler key.
Schema order is preserved. ['a', 'b'] and ['b', 'a'] intentionally produce different buildKeys.
Examples:
- A buildKey is a canonical string derived from connection identity, schemas, and role inputs:
{"conn":"127.0.0.1:5432/mydb@postgres","schemas":["services_public"],"anonRole":"administrator","roleName":"administrator"}- Different route keys can still share the same handler when they resolve to the same build inputs:
svc_key: tenant-a.example.com
svc_key: tenant-b.example.com
svc_key: api:main-db:services-api
svc_key: schemata:main-db:services_public
Each route key is first resolved into the inputs that matter for handler construction:
dbnameschemasanonRoleroleName
These then feed into the buildKey:
{"conn":"<host>:<port>/<dbname>@<user>","schemas":[...],"anonRole":"...","roleName":"..."}Different route keys only share a buildKey if they ultimately resolve to the same:
connschemasanonRoleroleName
In practice, the resolution rules differ by path:
- domain lookup /
X-Api-Nameusually resolve roles from the API record X-Schematauses administrator defaults and takes schemas directly from the header
For example, api:main-db:services-api and schemata:main-db:services_public
only share a handler if the services-api lookup ultimately resolves to the
same schema list and the same role settings. In many deployments, they do not.
- Schema order matters, so these produce different buildKeys:
{"conn":"127.0.0.1:5432/mydb@postgres","schemas":["services_public","metaschema_public"],"anonRole":"administrator","roleName":"administrator"}
{"conn":"127.0.0.1:5432/mydb@postgres","schemas":["metaschema_public","services_public"],"anonRole":"administrator","roleName":"administrator"}- Different database connections also produce different buildKeys, even when schema names match.
At runtime the cache maintains three main indexes:
buildKey -> TenantInstancesvc_key -> buildKeydatabaseId -> Set<buildKey>
The flow is:
- Compute the
buildKeyfrom pool identity, schemas, and role inputs. - Check the handler cache for an existing
buildKey. - If another request is already building that handler, await the shared promise.
- If no handler exists, create a fresh PostGraphile instance.
- Register the
svc_key -> buildKeymapping only after creation succeeds.
This means:
- different request keys can share one handler when build inputs are identical
- failed in-flight creation does not leave orphaned mappings
- stale
svc_keyrebindings can be evicted cleanly
- Fast path:
svc_key -> buildKey -> TenantInstance - Slow path: compute
buildKey, create/coalesce handler, then register mapping
The package supports:
- flushing one routed tenant by
svc_key - flushing all handlers associated with a
databaseId - full shutdown and disposal of cached handlers
| Export | Purpose |
|---|---|
configureMultiTenancyCache(config) |
Registers the base preset builder. Must be called before handler creation. |
getTenantInstance(svcKey) |
Fast-path lookup via svc_key. |
getOrCreateTenantInstance(config) |
Resolve or create a handler for a request. |
flushTenantInstance(svcKey) |
Evict the handler currently mapped to a route key. |
flushByDatabaseId(databaseId) |
Evict all handlers associated with a database. |
getMultiTenancyCacheStats() |
Return cache/index counts for diagnostics. |
shutdownMultiTenancyCache() |
Dispose handlers and clear all internal state. |
computeBuildKey(pool, schemas, anonRole, roleName) |
Compute the exact-match handler identity. |
getBuildKeyForSvcKey(svcKey) |
Resolve the buildKey currently mapped to a route key. |
MIT