@jerrick/workspaces
v0.9.22
Published
Auth, multi-tenancy, authorization, resources, billing, and lifecycle infrastructure for React and Next.js applications.
Downloads
285
Maintainers
Readme
@jerrick/workspaces
The multi-tenant application kernel for React and Next.js.
@jerrick/workspaces gives a product one coherent foundation for identity,
tenancy, authorization, billing, and the UI around them. Define the tenant
model once; the server, browser client, and generated Firestore rules all use
that same definition.
definition ──→ server authorization
├─→ browser hooks and UI
├─→ billing and entitlements
└─→ Firestore rules and indexesIt is framework-thin at its core: request handlers use standard Request and
Response objects. Next.js helpers are available where they make server
components and route mounting easier.
What it owns
| Area | Included |
| --- | --- |
| Identity | Firebase session cookies, email/password and OAuth sign-in, profile management, password resets, verification |
| Tenancy | Multiple workspace kinds, members, roles, capabilities, invites, API keys, lifecycle, account deletion |
| Authorization | Server require(...), a shared client evaluator, and generated Firestore rules |
| Billing | Plans, entitlements, usage, seats, Stripe Checkout/portal, Stripe and RevenueCat providers |
| Resources | Typed, workspace-scoped CRUD backed by Firestore or Postgres |
| Workspace presentation | A common branding value, form metadata, upload slots, and safe fallback resolution |
| Product surfaces | Realtime React hooks and brand-neutral UI for auth, teams, account settings, and workspace management |
Your application still owns its domain: product data, business workflows, media pipelines, analytics, and how workspace branding is applied to its public surfaces. The SDK only standardizes the portable value and editing mechanics. When an app needs to react to a kernel event, use a lifecycle event or configuration seam rather than placing app-specific code in this package.
The mental model
A workspace is the tenant: an organization, team, project, or any shared boundary in your product.
- Kind — a workspace type and its collection. Most apps have one; marketplaces may have several.
- Role — an ordered bundle of default capabilities. The first role is the owner role.
- Capability — an action such as
flows.manage. A bare string in an access check is always a capability. - Feature — a paid or plan-controlled product capability, such as seats, metered usage, or an entitlement.
Membership is stored on the workspace document as members: { uid: role }.
That is deliberately the one shape the server, client, and Firestore rules can
all evaluate.
Start in five files
Install the package and only the peers you use:
npm install @jerrick/workspaces firebase firebase-admin
# Add react for hooks/UI, stripe for Stripe billing, and postgres for Postgres resources.The npm package ships TypeScript source—the same inspectable source used by the fleet's vendored tarballs. A Next.js application should transpile each shared package it imports:
// next.config.ts
const nextConfig = {
transpilePackages: ["@jerrick/workspaces", "@jerrick/ui"],
};
export default nextConfig;Tailwind v4 must scan the whole package because definition-driven form classes
live under src/resources as well as src/ui:
/* app/globals.css — adjust the relative path for your app */
@source "../node_modules/@jerrick/workspaces/src";Do not narrow that path to src/ui; doing so can make the same SDK form lose
layout, color-picker, or upload styles in one consumer while looking correct in
another.
1. Define the tenant model
The definition must be pure and safe to import in the browser: no secrets,
Firebase Admin, Stripe client, environment variables, or server-only imports.
// lib/workspaces/organizations.ts
import { defineWorkspaces } from "@jerrick/workspaces";
export const definition = defineWorkspaces({
version: 1,
kinds: {
organization: {
collection: "organizations",
roles: {
owner: { label: "Owner", capabilities: ["*"] },
admin: {
label: "Admin",
capabilities: ["members.view", "members.invite", "members.manage", "flows.manage"],
},
member: { label: "Member", capabilities: ["members.view"] },
},
defaultRole: "member",
features: { invites: true, apiKeys: true },
},
},
} as const);Role order matters: declare the highest role first. Use the built-in operation
names for kernel behavior (workspace.view, workspace.update,
workspace.delete, members.manage, members.invite, apiKeys.manage,
domains.manage, and connect.manage); invent names only for your own
product gates.
2. Create one server
// lib/workspaces/server.ts
import "server-only";
import { createNextWorkspaceManager } from "@jerrick/workspaces/next";
import { createWorkspaceServer, type WorkspaceServer } from "@jerrick/workspaces/server";
import { definition } from "./organizations";
let cached: WorkspaceServer<typeof definition> | undefined;
export function workspaceServer() {
return (cached ??= createWorkspaceServer({
definition,
tokenPepper: process.env.WORKSPACES_TOKEN_PEPPER!,
}));
}
// Verified identity for Server Components, server actions, and layouts.
export const currentWorkspaces = createNextWorkspaceManager({ server: workspaceServer });WORKSPACES_TOKEN_PEPPER is a long, server-only secret used to digest invite
and API-key tokens. Rotating it invalidates all outstanding tokens. Firebase
Admin uses the SDK credential ladder by default, or you can pass explicit
Firebase services to the server factory.
3. Mount the SDK routes
// app/api/workspaces/[...segments]/route.ts
import { createWebHandler } from "@jerrick/workspaces/server";
import { workspaceServer } from "@/lib/workspaces/server";
export const { GET, POST, PATCH, DELETE } = createWebHandler(workspaceServer);// app/api/auth/[...auth]/route.ts
import { createAuthRoutes } from "@jerrick/workspaces/auth/server";
export const { GET, POST, PATCH } = createAuthRoutes();The SDK authenticates and authorizes these routes itself. Do not add wrapper
routes for built-in workspace, member, invite, API-key, or billing operations.
When you need an app-specific policy, add it to the handler's guarded() seam.
4. Bind the browser client
// lib/workspaces/client.ts
import { createWorkspaceClient } from "@jerrick/workspaces/react";
import { definition } from "./organizations";
export const {
WorkspacesProvider,
WorkspaceProvider,
useWorkspaceAccess,
useWorkspaces,
useWorkspaceMutations,
useMemberMutations,
} = createWorkspaceClient({ definition });// components/providers.tsx
"use client";
import { WorkspacesProvider } from "@/lib/workspaces/client";
export function Providers({ children }: { children: React.ReactNode }) {
return <WorkspacesProvider>{children}</WorkspacesProvider>;
}// A workspace route layout
import { WorkspaceProvider } from "@/lib/workspaces/client";
<WorkspaceProvider workspace={{ kind: "organization", id: organizationId }}>
{children}
</WorkspaceProvider>;5. Enforce on the server; project in the client
// Server Component, action, or route handler
const manager = await currentWorkspaces();
if (!manager) redirect("/auth/login");
const workspace = manager.workspace({ kind: "organization", id: organizationId });
await workspace.require("flows.manage");// Client component: realtime state drives the experience.
const access = useWorkspaceAccess();
access.can("flows.manage");
access.hasMinimumRole("admin");
access.hasFeature("reports");Client checks make the UI responsive; they are not the security boundary.
Always guard server work with require(...) or requireFeature(...). Never
treat a cookie's presence as proof of identity: use currentWorkspaces() in
server components or workspaceServer().forRequest(request) in route handlers.
6. Generate and deploy rules
Generate Firestore rules and indexes from the same definition, commit them, and deploy both. Read Firestore rules before deploying over a legacy data shape.
Add capabilities when you need them
| Need | Start here | | --- | --- | | Auth pages, session behavior, native auth | Authentication | | Members, roles, invites, API keys, server authorization | Authorization and access | | Workspace data, lifecycle events, deletion | Workspaces | | Plans, seats, usage, Checkout, webhooks | Billing | | Typed Firestore/Postgres workspace CRUD | Resources | | Hooks, providers, feature gates, reusable UI | React and UI | | Emails, account avatars, and workspace assets | Email and Storage | | Shared workspace logos, colors, fonts, and fallbacks | Workspace branding | | Workspace-owned credentials | Credential encryption | | Tenant domains and marketplace payments | Custom domains and Stripe Connect | | Every import, environment variable, and route | Reference |
For a guided product shell, copy apps/workspace-starter.
It is the minimal complete application with auth, workspace settings, invites,
billing, generated rules, and a secure route layout already wired.
Package map
| Import | Purpose |
| --- | --- |
| @jerrick/workspaces | Definition, types, authorization evaluator, errors, and paths |
| /server · /next | Server engine and Next.js server-component helpers |
| /auth/server · /auth/client · /auth/react · /auth/native | Cookie-first auth across web and Expo |
| /react · /ui · /ui/native | Realtime client state, gates, and reusable UI |
| /billing/* | Billing configuration, engine, and Stripe/RevenueCat providers |
| /resources/* | Typed resource definitions, server engine, clients, and Postgres store |
| /rules | Firestore rules and index generators |
| /email · /storage · /waitlist | Transactional email, workspace asset upload, and waitlist capture |
| /crypto | One-way token digests and workspace/field-bound credential encryption |
| /domains/* · /connect/* | Custom domains and Stripe Connect |
Operating principles
- One definition, everywhere. Do not duplicate roles or feature rules in routes, UI, or Firestore configuration.
- Server-first sessions. Credentials go to the server; the browser receives an httpOnly session cookie. Browser Firebase auth is derived from that session.
- Transactions protect membership. Never write the
membersmap or member documents outside SDK operations. - Gate features, not plan names.
requireFeature("reports")remains correct when pricing tiers change;plan === "pro"does not. - Deletion is deliberate. Workspace deletion cancels billing first, then emits lifecycle events so the product can remove its own domain data.
