npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dtfoundry/app-tenancy

v0.3.0

Published

Multi-tenant request layer for Digital Trust Foundry applications

Readme

@dtfoundry/app-tenancy

One deployment, many customers.

@dtfoundry/foundry-sdk is the client for one tenant's Foundry Platform. This package is the layer in front of it: it works out which tenant a request belongs to, checks that tenant has this application enabled and is not suspended, and hands the request that tenant's client — and no one else's.

Four pieces:

  • tenantMiddleware — resolves the tenant and puts req.tenantId, req.tenant and req.foundry on the request.
  • TenantRegistry — one FoundryClient per tenant, keyed by tenant and credential version, so a rotated key drops the old client with no restart.
  • createProvisionRouter/internal/tenants, the contract Foundry calls when a tenant enables, suspends or re-credentials this application.
  • createCredentialCipher — AES-256-GCM, so the stored Platform key is encrypted at rest and bound to the tenant it belongs to.

One rule: an application never constructs a FoundryClient itself. It asks the registry. A client built by hand is a client built from whatever credential was nearest, which is how one customer ends up reading another's data.

Install

pnpm add @dtfoundry/app-tenancy @dtfoundry/foundry-sdk

Node 20+, Express 4 or 5 (a peer dependency — this package bundles no framework).

Wiring it up

import {
  createCredentialCipher,
  createProvisionRouter,
  TenantRegistry,
  tenantMiddleware,
  tenantOf,
} from "@dtfoundry/app-tenancy";

const registry = new TenantRegistry({
  store, // your TenantStore — see below
  cipher: createCredentialCipher(process.env.CREDENTIAL_KEY!),
});

// Service-to-service. Mount it before the tenant middleware: Foundry calls it
// with a service token, not as a tenant.
app.use(
  "/internal/tenants",
  createProvisionRouter({
    registry,
    productSlug: "files",
    serviceToken: process.env.SERVICE_TOKEN,
  }),
);

app.use(
  tenantMiddleware(registry, {
    baseDomain: "digitaltrustfoundry.com",
    edgeSecret: process.env.EDGE_SECRET,
    skip: (req) => req.path === "/health",
  }),
);

app.get("/api/documents", async (req, res) => {
  const { tenant, foundry } = tenantOf(req);
  const trustables = await foundry.listTrustables({ limit: 20 });
  res.json({ tenant: tenant.tenantSlug, trustables });
});

Generate the credential key once per deployment and keep it out of the database the ciphertext lives in:

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

The store

The one interface an application implements. Each app keeps this in its own database, next to its own data.

interface TenantStore {
  findById(tenantId: string): Promise<StoredTenant | null>;
  findBySlug(tenantSlug: string): Promise<StoredTenant | null>;
  upsert(record: StoredTenant): Promise<StoredTenant>;
  update(tenantId: string, patch: TenantPatch): Promise<StoredTenant | null>;
  delete(tenantId: string): Promise<boolean>;
}

upsert must be idempotent on tenantId, and tenantSlug must be unique — both are lookup keys on the request path. encryptedApiKey is opaque: it arrives already encrypted and the store never sees the credential in plaintext.

How a tenant is recognised

X-DTF-Tenant (stamped by the edge router after it resolves the route) → subdomain → a dev-only override header.

The subdomain step needs baseDomain and reads X-DTF-Tenant-Host before Host, because the proxy chain rewrites Host and X-Forwarded-Host on the way through. files.uni.example.com and uni.example.com both name the tenant uni. A hostname outside the configured zone resolves to nothing.

The dev override (X-DTF-Dev-Tenant) is ignored unless allowDevOverride is explicitly true, so shipping it enabled takes a deliberate act.

Every step of that ladder is a request header. If the application's origin is reachable without going through the edge router — a platform-assigned hostname, an instance-prefixed name the router does not see — a caller can stamp X-DTF-Tenant themselves and be handed any provisioned tenant's client. Set edgeSecret to the shared secret the router stamps as X-DTF-Edge-Secret, and a request that does not present it resolves to no tenant at all. Leave it unset only where the origin genuinely cannot be reached except through the router.

edgeSecret also takes a list, which is what makes rotation possible: the router stamps one value at a time, so origins accept both across the window (edgeSecret: [current, next]), the router switches, then the old value comes out. Reversing that order takes down every origin that has not yet accepted the new value, until it does.

Leaving edgeSecret unset skips the check. Setting it to something with no usable value in it — "", [], a pair of environment variables that turned out to be empty — resolves no tenant at all, so a guard that was configured wrongly refuses requests rather than waving them through.

A request that resolves to no tenant is 400 TENANT_NOT_RESOLVED; one for a tenant this instance does not serve is 404 TENANT_NOT_FOUND; a suspended tenant is 403 TENANT_SUSPENDED. All in the standard envelope.

Provisioning

createProvisionRouter implements the contract Foundry calls:

| Route | Effect | | ------------------------------------ | ------------------------------------------------------------ | | POST / | Bind a tenant. Idempotent — a repeat re-binds and resumes. | | PATCH /:tenantId | Suspend, resume, or replace the config. | | DELETE /:tenantId | Unbind. Idempotent; the application's own data is untouched. | | POST /:tenantId/rotate-credentials | Replace the key. Caches are dropped before it answers 200. | | GET /:tenantId/health | Per-tenant database and Platform checks. |

Auth is a shared service token in X-Service-Token, compared in constant time. It fails closed: an instance started without one answers 503 to every route rather than accepting anything.

Licence

Apache-2.0