Many orgs behind one address
One app, many orgs: a directory runtime signs people in and knows their orgs, and each org is a runtime of its own, opened when used and closed when idle.
On this page 6
An app that serves many orgs runs a directory in front of them. Each org is a runtime of its own, with its own store.
The directory and its orgs
The directory (directory) is a runtime of its own. It signs people in and knows their orgs. The decisions behind it are in the tenancy design.
Each org is a runtime too. A pool of runtimes (runtimes, from copilotz) opens one when it's first used. It closes one after 30 minutes with no browsers connected and nothing pending, and opens it again on its next use. Two runtimes of one org never run at once, so its store has one writer.
The directory's fetch is the whole app. Its routes answer its own paths: /orgs, signing in and out, and staff's console. Everything else goes, untouched and sockets included, to the org's runtime.fetch. The directory finds the org 3 ways:
- by address,
/o/<org>/…; - by the browser's
orgcookie; - by a host the org claimed.
URLs and cookie names are the same as with one runtime. directory.member keeps them apart per org.
The code
The directory is its own package, @copilotz/directory, at Copilotz's version (npm install @copilotz/directory, deno add jsr:@copilotz/directory).
import { createRuntime, definePlugin, type Runtime, runtimes } from "copilotz";
import { memoryStore } from "copilotz/stores/memory";
import { devSignIn, identity } from "copilotz/plugins/access";
import { directory, type OrgInfo } from "@copilotz/directory";
import { webPage } from "copilotz/web/server";
import { themeOf } from "copilotz/web/theme";
import { bundle, listen } from "copilotz/web/node";
import { plugins, storeOf } from "./app.ts"; // the app's plugins, and where an org's rows live
// The page, the same for the lobby and every org (each at its own address).
const code = await bundle(new URL("./browser.tsx", import.meta.url));
const page = [
webPage,
definePlugin({ name: "page", resources: { page: { code }, theme: themeOf([]) } }),
];
// Each org's runtime, opened on its first use, closed after 30 minutes nobody used it.
const orgs = runtimes(async (id) => {
const org = await dir.query<OrgInfo | null>("org.info", { org: id });
if (!org) return undefined;
return await createRuntime({
store: await storeOf(org),
plugins: [...plugins, ...page, directory.member(dir, org)],
});
});
// What someone signed in nowhere gets: the sign-in page.
const lobby = await createRuntime({ store: memoryStore(), plugins: page });
const dir: Runtime = await createRuntime({
store: memoryStore(),
plugins: [
identity,
directory,
devSignIn,
definePlugin({
name: "directory-settings",
resources: { directory: { orgs, lobby, staff: ["you@example.com"] } },
}),
],
});
const server = await listen(dir.fetch, { port: 8080 }); // or Deno.serve(dir.fetch)What each org's runtime gets
directory.member(dir, org) gives each org's runtime 4 things:
- which org it is, as
resources.org; - where it's served, as
page.base:/o/<org>; - its cookies' names:
s_<org>for its session, and<name>_<org>for the rest; - the directory's hooks for invitations, leaving and renaming.
Hosts
Name the app's own hosts with the hosts resource, such as { "app.example.com": true }. Any other host is then an org's claimed one, answered by that org's host routes alone.
What stays in the directory
Whatever has one address for every org lives in the directory's runtime. Stripe's webhook is one (stripeWebhook from @copilotz/billing, with the stripe resource's orgs). The cloud pool's socket is another.
In the browser, directoryWeb (from @copilotz/directory/web) fills the web shell's orgs slot: the org switcher, the org's page and staff's console. Without it, the shell shows no orgs.
Examples
examples/web-node/orgs.tsruns it in memory, on Node.examples/terminal-agent/orgs.tsruns it on Deno, with each org a tenant of one PostgreSQL schema, and staff's operations.
Build Compass, Chapter 41 builds it step by step, with each org's usage priced in that org alone.