Sub-App Development Guide
If you are about to build a new Sub-App, read this first.
This directory is the backend entry for bearer-link Sub-Apps. The most important rule is:
A public Sub-App page does not mean the database should be public.
Core Model
Sub-App request flow:
- Agent tool creates an instance.
- The tool generates a signed bearer link and sends it to the IM channel.
- User opens
/app/{slug}/{id}?t=...with no login required. - The page calls
/api/app/{slug}with that token. - The server verifies the token and reads/writes with
service_role. - If realtime is needed, the page calls
/api/app/{slug}/sessionto exchange the signed token for a short-lived Realtime JWT.
Recommended File Layout
Frontend page: src/app/app/{slug}/[id]/page.tsx
Backend API: src/app/api/app/{slug}/route.ts
Realtime API: src/app/api/app/{slug}/session/route.ts
Token utils: src/lib/{slug}-token.ts
Realtime JWT: src/lib/{slug}-realtime.ts
Sub-App config: src/lib/sub-app-settings.ts + public.sub_app_settings
Security Baseline
/app/*pages are public./api/app/*routes are public.- Business tables are private by default.
- The browser does not query business tables directly with the anon key.
- The server verifies the signed token on every request.
- The server performs real DB access with
createStrictServiceClient().
Do not grant anon direct access on business tables, expose tables through public postgres_changes, or silently fall back to weaker behavior.
Database Pattern
ALTER TABLE public.your_slug_instances ENABLE ROW LEVEL SECURITY;
CREATE POLICY "your_slug_instances_admin_all"
ON public.your_slug_instances
FOR ALL USING (public.is_admin());
CREATE POLICY "your_slug_instances_service_all"
ON public.your_slug_instances
FOR ALL USING (current_setting('role') = 'service_role');
GRANT ALL ON public.your_slug_instances TO service_role;
REVOKE ALL ON TABLE public.your_slug_instances FROM PUBLIC, anon, authenticated;
Backend API Rules
- Verify the signed token on every request.
- Use
createStrictServiceClient()for security-sensitive reads and writes. - Return
503if Sub-App config is incomplete. - If you use
after(), keep the route on Node.js runtime.
Frontend Realtime Pattern
For bearer-link Sub-Apps, prefer private Broadcast + Presence:
const session = await fetch("/api/app/your-slug/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ instance_id, token }),
}).then((res) => res.json());
await supabase.realtime.setAuth(session.realtimeJwt);
const channel = supabase
.channel(session.topic, { config: { private: true } })
.on("broadcast", { event: "INSERT" }, handlePayload)
.on("presence", { event: "sync" }, handlePresence);
Agent Tool Rules
- Tools must send user-facing URLs directly with
sender.sendMarkdown(). - Tool strings must be internationalized.
- Use explicit Markdown links.
- Add Sub-App tool policy in
tooling/runtime.ts.