Frontend AGENTS.md
High-signal constraints for the relay-server frontend. Only items expensive to rediscover.
Frontend-Backend Contracts
Public list data comes from
/ui/state. Go relay returns leases only;api/server.tsadds frontend-owned presentation fields mirrored insrc/types/api.ts.- Why: the Go relay is API-only. Do not reintroduce Go HTML data injection for public lease state.
Relay and presentation API paths have separate owners. Relay paths are owned by
../types/paths.goand mirrored insrc/lib/apiPaths.tsfor browser calls; presentation paths stay under/ui/insrc/lib/apiPaths.ts,api/server.ts, and the edge nginx template.- Why: nginx should distinguish the BFF by one prefix instead of enumerating presentation endpoints.
API envelope shape must match across Go and TS. All JSON control-plane responses use
{ ok, data?, error?: { code, message } }. Go shape istypes.APIEnvelopein../types/api.go; Go writers live in../utils/api.go; TS parser lives insrc/lib/apiClient.ts.- Why: backend responses that skip the envelope surface as
invalid_envelopein the frontend.
- Why: backend responses that skip the envelope surface as
Admin auth uses bearer tokens returned by
/api/admin/auth/login.src/hooks/useAuth.tsstores the token throughsrc/lib/adminAuthToken.ts;src/lib/apiClient.tsadds it to/api/admin/*,/api/policy/*, and/ui/policy/*requests asAuthorization: Bearer ....- Why: the relay admin API must be usable by any separately hosted frontend without credentialed cookie CORS state.
VITE_PORTAL_API_BASE_URLis the only built-in API origin knob. Leave it empty for same-origin development/proxying, or set it at build/dev time to the public edge origin/base path. Do not point it at/api;/api,/ui,/sdk, and/discoveryare sibling paths.- Why: runtime-generated config files couple the static frontend bundle back to deployment state.
Presentation policy state reads are aggregated through
/ui/policy/state.src/hooks/useAdmin.tsexpects{ policy, leases }; presentation policy settings writes go through/ui/policy, while lease/IP actions use/ui/policy/leasesand/ui/policy/ips.- Why: splitting those reads across multiple endpoints reintroduces extra request coordination and drift in the admin bootstrap path.
Lease/policy lease JSON casing is snake_case. Go
Lease/PolicyLeaseJSON tags live in../types/identity.go; TS mirrors the wire shape insrc/types/api.ts.- Why: the frontend should not depend on Go's implicit PascalCase encoder output.
Admin policy writes identify targets in the JSON body. Presentation lease policy writes use
/ui/policy/leaseswithidentity_key; IP policy writes use/ui/policy/ipswithip, thenapi/server.tsforwards to relay/api/policy/*.- Why: path encoding rules add a second contract surface and are easy to drift across Go and TS.
Lease metadata has a wire type and a UI parser. Go
LeaseMetadata(../types/identity.go) mirrors TSLeaseMetadata(src/types/api.ts). UI display defaults are owned bysrc/lib/metadata.ts.- Why: API contract fields and UI fallback behavior should not be mixed.
Presentation support is frontend-owned.
api/server.tsserves/ui/state,/ui/policy/*,/ui/service/status, and/ui/thumbnail/{hostname}by composing relay data with frontend-owned state.- Why: landing-page flags, quick-start status, and generated screenshots are presentation support and should not add state or routes to the Go relay API.
ApprovalMode is a closed two-value enum:
"auto"|"manual". TSnormalizeApprovalMode()(src/hooks/useAdmin.ts) collapses any non-"manual"value to"auto".- Why: adding a third mode in Go without updating the TS normalizer silently collapses it to "auto".
Frontend Conventions
Do not use
useCallbackin new code. React Compiler (babel-plugin-react-compiler, enabled invite.config.ts) handles memoization automatically.- Why: manual
useCallbackis redundant with the compiler and adds noise.
- Why: manual
Feature state lives in page-level hooks and is prop-drilled. No global state library.
useServerList,useAdmin, anduseAuthown feature state at the page level. Theme is the exception; it usesThemeProvider.localStoragepersistence should silently fall back on errors.- Why: the prop-drilling pattern for feature state is intentional. Adding shared state providers for feature data changes the data flow architecture.
Only
handleBPSChangeuses optimistic update with rollback. All other admin actions userunAdminAction()which awaits the API call then refreshes viafetchData().- Why: treating other admin handlers as optimistic will skip the server-refresh step and show stale data.