# Enterprise / private code Pangolin builds three ways, chosen by `server/build.ts` (generated by `npm run set:oss|enterprise|saas`): - `oss`: open source (AGPLv3). `server/private/` is **not** part of this build. - `enterprise`: self-hosted commercial build. Includes `server/private/`. - `saas`: Pangolin Cloud. Includes `server/private/`. Check the build at runtime with `import { build } from "@server/build"` (`build === "oss"`, `"enterprise"`, `"saas"`). The UI imports the same constant. ## The `server/private/` directory - Everything under `server/private/` is proprietary and only compiled into the enterprise and saas builds. - Every file there must start with the Fossorial Commercial License header. `license_header_checker.py` enforces this. Copy the header from any existing private file. - Its layout mirrors `server/`: `private/routers//`, `private/lib/`, `private/middlewares/`, `private/auth/`, `private/license/`. - The private router files (`server/private/routers/external.ts`, `internal.ts`, `integration.ts`, `hybrid.ts`) import the public routers, add the private routes, and re-export them. The servers load them through `#dynamic/routers/external` and so on. ## Import rules | You are in… | To import a private file | To import a public file | | --- | --- | --- | | `server/private/**` | `#private/` (**never** `#dynamic`) | `@server/` | | anywhere else (`server/**`, `src/**`) | **never** use `#private`. Use `#dynamic/` | `@server/` | `esbuild.mjs` enforces both rules: `privateImportGuardPlugin` flags `#private` imports outside `server/private`, and `dynamicImportGuardPlugin` flags `#dynamic` imports **inside** `server/private` (private code already knows it wants the private file, so it uses `#private`). ### How `#dynamic` works `#dynamic/` resolves at build time: - `oss` → `server/` - `enterprise` / `saas` → `server/private/` (In `tsconfig.oss.json` it maps to `./server/*`. In `tsconfig.enterprise.json` / `tsconfig.saas.json` it maps to `./server/private/*`.) So any `#dynamic` import needs **two files at the same relative path with the same exports**: 1. `server/private/.ts`: the real enterprise implementation (with the license header). It can import other private code via `#private/...`. 2. `server/.ts`: the OSS version. Usually a stub that no-ops or returns a safe default. Sometimes a simpler working version. Examples: | `#dynamic` import | OSS file | Private file | OSS behaviour | | --- | --- | --- | --- | | `#dynamic/middlewares` (`logActionAudit`) | `server/middlewares/logActionAudit.ts` | `server/private/middlewares/logActionAudit.ts` | Middleware just calls `next()` | | `#dynamic/lib/isLicencedOrSubscribed` | `server/lib/isLicencedOrSubscribed.ts` | `server/private/lib/isLicencedOrSubscribed.ts` | Always `false` | | `#dynamic/lib/blueprints/MaintenanceSchema` | `server/lib/blueprints/MaintenanceSchema.ts` | `server/private/lib/blueprints/MaintenanceSchema.ts` | Empty `z.object({})` | | `#dynamic/lib/cache`, `lib/lock`, `lib/rateLimitStore` | in-memory / local | Redis-backed | Single-node implementations | | `#dynamic/lib/traefik` | `server/lib/traefik/` | `server/private/lib/traefik/` | Different config generator | Rules of thumb: - Keep the exported names and signatures identical, or the build that uses the other file breaks. Type-check under both builds: `npm run set:oss && npx tsc --noEmit`, then `npm run set:enterprise && npx tsc --noEmit`. - If `index.ts` re-exports (e.g. `server/middlewares/index.ts`), make sure the private `index.ts` re-exports the same names. - Don't scatter `if (build === "oss")` checks through public code that would need private logic. Put the difference behind a `#dynamic` module. - A private-only route doesn't need a stub. Register it in the `server/private/routers/*.ts` files and it simply doesn't exist on OSS. ## Private config - Public config (`config/config.yml`) is validated by `server/lib/readConfigFile.ts` and read through `config.getRawConfig()` from `@server/lib/config`. - **Private config** (`config/privateConfig.yml`) is validated by `privateConfigSchema` in `server/private/lib/readConfigFile.ts` and read through `privateConfig.getRawPrivateConfig()` from `#private/lib/config`. - Put enterprise-only config keys in the private schema, not the public one. If the UI needs a value, `PrivateConfig`'s constructor copies it to `process.env`, and `src/lib/pullEnv.ts` reads it from there. ## Private database tables Tables used only by enterprise features go in `server/db/pg/schema/privateSchema.ts` **and** `server/db/sqlite/schema/privateSchema.ts`. Both are re-exported from `@server/db`, so imports are the same as for public tables. The parity rules in `docs/database.md` apply. ## UI side - Enterprise-only pages live under the `src/app/[orgId]/settings/(private)/` route group. - Gate enterprise UI with `env.flags.disableEnterpriseFeatures` (from `useEnvContext()` / `pullEnv()`) and tier checks with `usePaidStatus().isPaidUser(tierMatrix[TierFeature.X])`. Show `` above paywalled controls. See `src/app/[orgId]/settings/general/page.tsx` for the pattern. - Server-side tier enforcement uses `isLicensedOrSubscribed(orgId, tiers)` from `#dynamic/lib/isLicencedOrSubscribed`.