mirror of
https://github.com/fosrl/pangolin.git
synced 2026-11-10 01:32:29 -06:00
111 lines
5.3 KiB
Markdown
111 lines
5.3 KiB
Markdown
# 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/<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/external` and 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**:
|
|
|
|
1. `server/private/<path>.ts`: the real enterprise implementation (with the
|
|
license header). It can import other private code via `#private/...`.
|
|
2. `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`, 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
|
|
`<PaidFeaturesAlert tiers={tierMatrix.x} />` 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`.
|