Independent backend = its own process + its own DB, consuming the TDT issued by Portal. This file covers the operating sequence and red lines; read the specific contracts as needed references/(a self-contained reference distilled for this skill; the authoritative source is the portal repository docs/, and in case of conflict the portal repository prevails). This skill describeswithin the portal monorepodual-package pattern—when the server is in another repo, use the portal-external-app skill instead.
Read references by task
| Task | Read |
|---|---|
| introspection contract / four gates / service account / client_credentials / revocation | references/gate-and-tokens.md |
| registration wiring / scope catalog / deployment wiring / API conventions / usage reporting / config tab | references/wiring-and-conventions.md |
Reference implementation:apps/spms-server/src/lib/gate.ts + identity.ts(the isomorphic auth layer of each server).
Full-chain checklist for creating a new backend App
Complete in order; each step has an existing template to copy (for light business copy todo-server, for heavy business copy lms-server):
- dual package:
apps/<key>-server(Bun + Elysia, dev port takes a free 4xxx) +apps/<key>-app(for the frontend see the portal-micro-app skill). DB namingxgent-<key>。 - auth layer: copy
spms-server/src/lib/gate.ts+identity.tswholesale, change the listingKey/scope constants. Do not hand-write introspection logic. - Checklist and wiring(for locations and rules see wiring-and-conventions.md §1): ACL Manifest →
manifests.ts;listing →LISTING_DEFS; service account →SA_DEFS; exchange →EXCHANGE_WIRING; scope can only fall within its own namespace. - Seed: write the App's own seed/bootstrap as anon-destructivestandalone script (
db:seedis portal-only + destructive). - dev:all: hook server+app into the root
package.json's concurrently; never run a single dev script stacked. - Deployment wiringdo it all at once (Caddy allowlist / compose profile /
DEPLOYABLE_APP_SERVICES/ Dockerfile COPY new workspace /build:appsin series)—for per-item symptoms see wiring-and-conventions.md §2; miss one and production will 404/fail to start. - Verification script:
apps/<key>-server/scripts/verify-*.tscovers TDT/isolation/ACL/core business, hook it intoapps/api/scripts/verify-all.ts. A feature without a verify script does not count as delivered.
Four gates (every protected endpoint must pass)
Introspection POST /api/tokens/introspect(service account Basic) then: ① (claims.listingKey ?? claims.aud) === 本服务 listingKey; ② required scope ∈ claims.scopes; ③ for structural management operations check claims.role === "admin"; ④ fine-grained bypass || permissions 命中 PID(data scope takes the widest).
If the endpoint is aplatform-level cross-tenantoperation, add beyond the four gates claims.isPlatformAdmin === true: this value is the global identity that Portal computes in real time on each introspection based on user_id , and is not in the JWT.role / bypass only represents the TDT's current tenant and cannot replace this gate; nor does it accept self-reported frontend headers/fields.
Getting it wrong is the hard shape of a full 401:
- the introspection response goes through a unified envelope,declared in
datain:claims = body.data ?? body; active:falseissuccessfulintrospection (invalid token), not a transport error; result cached ≤60s;- Service-state TDT (
kind:"service", nouser_id)authorized by scope only, do not go through PID gate; active:trueintrospection result always carries booleanisPlatformAdmin; service state is alwaysfalse, identity change propagation is constrained by downstream ≤60s cache limit;- Tenant isolation always
claims.tenant_id, never trust the request body; rate limiting self-built(aud, tenant)per-minute window.
For complete fields and service account details see references/gate-and-tokens.md。
API and data conventions (mandatory review, details in wiring-and-conventions.md §3)
- Business state does not use HTTP status codes: always
200 + {ok, data|error}; data not found =200 + data:nullnot 404. - List endpoints use server-side pagination (
Page<T>); dictionary tables uniformlysort(tree/sibling order preservedseq)。 - migrationhand-written SQL(drizzle-kit generate is known broken in this repo).
- Audit writes
POST /api/v1/audit, do not self-build auditing; tenant configuration goes through the application configuration tab trio (wiring-and-conventions.md §5); do not do "contact sync"; metering goes through usage reporting (wiring-and-conventions.md §4).
Verification
Change complete = that server's verify-*.ts all green + bun run verify:all no regression + (when there is a UI surface) browser walkthrough. Dev key points: introspection prefers service account Basic (x-resource-key only DEV_MOCK_OAUTH=true and recognizes only FILES_RESOURCE_KEY one); backend script login uses a user without 2FA (e.g. liming)。