5.3 KiB
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. Includesserver/private/.saas: Pangolin Cloud. Includesserver/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.pyenforces this. Copy the header from any existing private file. - Its layout mirrors
server/:private/routers/<entity>/,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/externaland so on.
Import rules
| You are in… | To import a private file | To import a public file |
|---|---|---|
server/private/** |
#private/<path> (never #dynamic) |
@server/<path> |
anywhere else (server/**, src/**) |
never use #private. Use #dynamic/<path> |
@server/<path> |
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/<path> resolves at build time:
oss→server/<path>enterprise/saas→server/private/<path>
(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:
server/private/<path>.ts: the real enterprise implementation (with the license header). It can import other private code via#private/....server/<path>.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, thennpm run set:enterprise && npx tsc --noEmit. - If
index.tsre-exports (e.g.server/middlewares/index.ts), make sure the privateindex.tsre-exports the same names. - Don't scatter
if (build === "oss")checks through public code that would need private logic. Put the difference behind a#dynamicmodule. - A private-only route doesn't need a stub. Register it in the
server/private/routers/*.tsfiles and it simply doesn't exist on OSS.
Private config
- Public config (
config/config.yml) is validated byserver/lib/readConfigFile.tsand read throughconfig.getRawConfig()from@server/lib/config. - Private config (
config/privateConfig.yml) is validated byprivateConfigSchemainserver/private/lib/readConfigFile.tsand read throughprivateConfig.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 toprocess.env, andsrc/lib/pullEnv.tsreads 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(fromuseEnvContext()/pullEnv()) and tier checks withusePaidStatus().isPaidUser(tierMatrix[TierFeature.X]). Show<PaidFeaturesAlert tiers={tierMatrix.x} />above paywalled controls. Seesrc/app/[orgId]/settings/general/page.tsxfor the pattern. - Server-side tier enforcement uses
isLicensedOrSubscribed(orgId, tiers)from#dynamic/lib/isLicencedOrSubscribed.