mirror of
https://github.com/fosrl/pangolin.git
synced 2026-11-09 10:51:53 -06:00
5.8 KiB
5.8 KiB
Pangolin — agent guide
Pangolin is an identity-aware reverse proxy / VPN with a dashboard. The repo holds
an Express API server (server/) and a Next.js dashboard (src/) that share
types via path aliases. Read this file first. It holds the hard rules and points
to the detailed docs so you don't have to search for where things live.
Detailed docs (read the one that matches your task)
| Topic | File |
|---|---|
| Where everything lives (directory map) | docs/repo-map.md |
| Public vs private resources, tables, routes, blueprint sync | docs/resources.md |
Enterprise code: server/private, #private, #dynamic, stubs, private config |
docs/enterprise-private-code.md |
| DB schema (SQLite + Postgres parity), migrations, encrypting secrets | docs/database.md |
| Dashboard UI conventions (Credenza, banners, tabs, pages vs modals, i18n) | docs/ui-conventions.md |
| Building a CRUD endpoint | docs/crud-endpoints.md |
Hard rules
Naming
- Public resource = proxy resource =
resource, stored in theresourcestable, code inserver/routers/resource/. - Private resource = site resource = client resource =
siteResource, stored in thesiteResourcestable, code inserver/routers/siteResource/. - "Every resource" means both kinds: a change to resources usually needs the same change on site resources.
Resources and blueprints
- When you change
createResource/updateResourceorcreateSiteResource/updateSiteResource(new field, renamed field, new mode), also update the blueprint schema and apply/export logic so the same thing can be done from a blueprint. See the checklist indocs/resources.md. - Every resource route on the integration API (
server/routers/integration.ts,server/private/routers/integration.ts) needs apublic-resourcealias next to the legacyresourcepath. Site resource routes need aprivate-resourcealias next tosite-resource. Register both paths in OpenAPI.
Database
- The app runs on SQLite or Postgres. Every table change goes in both
server/db/pg/schema/*.tsandserver/db/sqlite/schema/*.ts, and the two must match. - Use only portable column types. No Postgres-only features: no
timestamp/datecolumns, nojson/jsonb, no enums, no arrays. Store times as epoch numbers (bigint({mode:"number"})in pg,integerin sqlite) or ISO strings (varchar/text). Store JSON astext. - Never write migrations (
server/setup/scripts*/,migrations*.ts, SQL). They're generated for each release with drizzle. Instead, write migration notes inserver/setup/migrationNotes/<next-untagged-version>.md, or update that file if it already exists. The notes cover what drizzle can't do: copying values, generating values for existing rows, renames, backfills, encryption, config changes. Seedocs/database.md§ Migration notes. - Encrypt sensitive values (secrets, API keys, client secrets, passwords that must be recovered) with
encrypt/decryptfrom@server/lib/crypto, keyed withconfig.getRawConfig().server.secret!. Values that only need verifying get hashed instead.
Enterprise / private code
- Everything in
server/private/ships only in the enterprise and SaaS builds. Every file there needs the Fossorial Commercial License header (seelicense_header_checker.py). - Inside
server/private/, import other private files with#private/.... Never use#dynamicthere. - Outside
server/private/, never import#private. Use#dynamic/.... It resolves toserver/private/<path>on enterprise/SaaS builds andserver/<path>on OSS builds. So you need both files: the real one inserver/private/...and an OSS stub (or reduced version) at the same relative path inserver/...with the same exports. - Private config values go in
server/private/lib/readConfigFile.ts(privateConfigSchema) and are read throughprivateConfigfrom#private/lib/config. They don't go in the publicserver/lib/readConfigFile.ts. Private DB tables go inserver/db/{pg,sqlite}/schema/privateSchema.ts.
UI
- Translate every user-facing string with
t("key")(useTranslations()in client components,await getTranslations()fromnext-intl/serverin server components). Don't hard-code strings in the UI, including toasts, labels, placeholders, descriptions and tab titles. - Never write translations. Add new keys only to
messages/en-US.json. Never edit the othermessages/*.jsonfiles; Crowdin generates them. - Use Credenza (
@app/components/Credenza) for every modal or dialog. Don't useDialog,Sheet,DrawerorAlertDialogdirectly. - For informational or help content on a page, use a
DismissableBanner(@app/components/DismissableBanner), not anAlert. - If an object has more than a handful of settings, give it pages (a list page, a create page, and
[id]/<tab>pages with alayout.tsx+HorizontalTabs) rather than a modal. Keep Credenzas for small, quick forms and confirmations. - Forms use
react-hook-formwithzodResolverand a zod schema. - Submit buttons use the
loadingprop. Don't change the button text while loading. - Prefer
typeoverinterfacein TypeScript.
API verb convention
PUT = create, POST = update. Don't "fix" this.
Commands
npm run devruns the API server (tsx watch). The Next app is served by the same process.npx tsc --noEmittype-checks. The active build and DB driver come fromtsconfig.jsonandserver/build.ts/server/db/index.ts, which are gitignored and generated bynpm run set:{oss,enterprise,saas}andnpm run set:{sqlite,pg}. Switch builds with those scripts instead of editing the files by hand. Type-check under bothset:ossandset:enterprisewhen you touch#dynamiccode.npm run format/npm run format:checkrun Prettier (4-space indent).npm testruns the tests.