api-tier-architecture
3-tier API architecture (Convex WebSocket, SSE, REST) for cross-platform data fetching. Platform detection, hybrid hooks, DAL layer patterns. Triggers on "API", "tier", "Convex", "REST", "SSE", "useConvexQuery", "useQuery", "withAuth", "DAL".
Best use case
api-tier-architecture is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
3-tier API architecture (Convex WebSocket, SSE, REST) for cross-platform data fetching. Platform detection, hybrid hooks, DAL layer patterns. Triggers on "API", "tier", "Convex", "REST", "SSE", "useConvexQuery", "useQuery", "withAuth", "DAL".
Teams using api-tier-architecture should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/api-tier-architecture/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How api-tier-architecture Compares
| Feature / Agent | api-tier-architecture | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
3-tier API architecture (Convex WebSocket, SSE, REST) for cross-platform data fetching. Platform detection, hybrid hooks, DAL layer patterns. Triggers on "API", "tier", "Convex", "REST", "SSE", "useConvexQuery", "useQuery", "withAuth", "DAL".
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# API Tier Architecture
Three-tier API architecture for web (real-time) and mobile (battery-optimized) platforms.
## Architecture Overview
**Tier 1 (Web Desktop):** Convex WebSocket - Real-time bidirectional subscription
**Tier 2 (Mobile):** SSE - Server-Sent Events with polling (battery-optimized)
**Tier 3 (Mobile Fallback):** REST - Standard HTTP polling
All tiers authenticated via `withAuth` middleware, data accessed via DAL layer.
## Platform Detection
```typescript
// From apps/web/src/lib/utils/platform.ts
export function shouldUseConvex(): boolean {
return getDataFetchingStrategy() === "convex";
}
export function shouldUseSSE(): boolean {
return getDataFetchingStrategy() === "sse";
}
// Detection hierarchy:
// 1. User-agent (iPhone, Android, mobile browsers)
// 2. Viewport width (< 768px)
// 3. Touch capability
```
**Manual override for testing:**
```typescript
localStorage.setItem("blah_data_strategy", "convex"); // or "sse" or "polling"
```
## Hybrid Hook Pattern
All data hooks use hybrid pattern: Convex for web, React Query for mobile.
```typescript
// From apps/web/src/lib/hooks/queries/useConversations.ts
export function useConversations(options: UseConversationsOptions = {}) {
const { page = 1, pageSize = 20, archived = false } = options;
const useConvexMode = shouldUseConvex();
const apiClient = useApiClient();
// Tier 1: Convex WebSocket subscription (web desktop)
const convexData = useConvexQuery(
api.conversations.list,
useConvexMode && !archived ? {} : "skip",
);
// Tier 2/3: REST API query (mobile)
const restQuery = useQuery({
queryKey: ["conversations", { page, pageSize, archived }],
queryFn: async () => {
const params = new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
archived: String(archived),
});
return apiClient.get(`/conversations?${params}`);
},
enabled: !useConvexMode,
staleTime: 30_000, // 30s cache
});
// Return unified interface
if (useConvexMode) {
return {
data: convexData ? { items: convexData, ... } : undefined,
isLoading: convexData === undefined,
error: null,
refetch: () => Promise.resolve(),
};
}
return {
data: restQuery.data,
isLoading: restQuery.isLoading,
error: restQuery.error,
refetch: restQuery.refetch,
};
}
```
**Key conventions:**
- Import both `useQuery` from `@tanstack/react-query` and `useQuery as useConvexQuery` from `convex/react`
- Check `shouldUseConvex()` before rendering
- Pass `"skip"` to Convex query when disabled
- Return unified interface: `{ data, isLoading, error, refetch }`
## DAL Layer (Data Access Layer)
Server-only Convex client wrappers. Never import in client components.
```typescript
// From apps/web/src/lib/api/dal/conversations.ts
import "server-only";
export const conversationsDAL = {
create: async (_userId: string, data: CreateInput) => {
const validated = createConversationSchema.parse(data);
const convex = getConvexClient();
const conversationId = (await (convex.mutation as any)(
// @ts-ignore - TypeScript recursion limit with 94+ Convex modules
api.conversations.create,
{ ...validated },
)) as any;
const conversation = (await (convex.query as any)(
// @ts-ignore - TypeScript recursion limit with 94+ Convex modules
api.conversations.get,
{ conversationId },
)) as any;
return formatEntity(conversation, "conversation", conversation._id);
},
getById: async (userId: string, conversationId: string) => {
const convex = getConvexClient();
// Uses clerkId for server-side ownership verification
const conversation = (await (convex.query as any)(
// @ts-ignore - TypeScript recursion limit with 94+ Convex modules
api.conversations.getWithClerkVerification,
{ conversationId: conversationId as Id<"conversations">, clerkId: userId },
)) as any;
if (!conversation) {
throw new Error("Conversation not found or access denied");
}
return formatEntity(conversation, "conversation", conversation._id);
},
// Always verify ownership before mutations
update: async (userId: string, conversationId: string, data: UpdateInput) => {
await conversationsDAL.getById(userId, conversationId); // Ownership check
// ... perform mutation
},
};
```
**DAL conventions:**
- Always validate input with Zod schemas
- Use `(convex.mutation as any)` + `@ts-ignore` for type recursion workaround
- Always wrap responses with `formatEntity(data, "entityName", id)`
- Verify ownership before mutations (call `getById` first)
- For mutations requiring `ctx.auth`, use `getAuthenticatedConvexClient(sessionToken)`
## REST API Routes (Tier 3)
```typescript
// From apps/web/src/app/api/v1/conversations/route.ts
async function postHandler(req: NextRequest, { userId }: { userId: string }) {
const startTime = performance.now();
logger.info({ userId }, "POST /api/v1/conversations");
const body = await parseBody(req, createSchema);
const result = await conversationsDAL.create(userId, body);
const duration = performance.now() - startTime;
trackAPIPerformance({
endpoint: "/api/v1/conversations",
method: "POST",
duration,
status: 201,
userId,
});
return NextResponse.json(result, { status: 201 });
}
async function getHandler(req: NextRequest, { userId }: { userId: string }) {
const limit = Number.parseInt(getQueryParam(req, "limit") || "50", 10);
const archived = getQueryParam(req, "archived") === "true";
const conversations = await conversationsDAL.list(userId, limit, archived);
return NextResponse.json(
formatEntity({ items: conversations, total: conversations.length }, "list"),
{
headers: {
"Cache-Control": getCacheControl(CachePresets.LIST), // 30s cache
},
},
);
}
export const POST = withErrorHandling(withAuth(postHandler));
export const GET = withErrorHandling(withAuth(getHandler));
export const dynamic = "force-dynamic";
```
**REST conventions:**
- Wrap handlers with `withAuth` (requires authentication) or `withOptionalAuth`
- Wrap with `withErrorHandling` for consistent error responses
- Parse body with `parseBody(req, zodSchema)`
- Always call `trackAPIPerformance` for monitoring
- Use structured logging with `logger.info/warn/error`
- Return envelope-formatted responses via `formatEntity`
- Set `dynamic = "force-dynamic"` to prevent static optimization
## SSE Routes (Tier 2)
For medium-duration operations with real-time progress updates.
```typescript
// From apps/web/src/app/api/v1/conversations/stream/route.ts
async function getHandler(req: NextRequest, { userId }: { userId: string }) {
const convex = getConvexClient();
// Create SSE connection
const { response, send, sendError, close, isClosed } = createSSEResponse();
try {
// Send initial snapshot
const initialData = await convex.query(api.conversations.list, {});
await send("snapshot", { conversations: initialData });
// Poll for updates every 5s
const pollInterval = createPollingLoop(
async () => {
if (isClosed()) return null;
const conversations = await convex.query(api.conversations.list, {});
return { conversations };
},
send,
5000, // 5s polling
"update",
);
// Heartbeat every 2min (prevents mobile carrier disconnection)
const heartbeat = createHeartbeatLoop(send, 120_000);
// Setup cleanup on disconnect
setupSSECleanup(req.signal, close, [pollInterval, heartbeat]);
return response;
} catch (error) {
await sendError(error instanceof Error ? error : new Error(String(error)));
await close();
return new Response("Internal server error", { status: 500 });
}
}
export const GET = withErrorHandling(withAuth(getHandler));
```
**SSE patterns:**
1. `createSSEResponse()` - Returns `{ response, send, sendError, close, isClosed }`
2. Send initial snapshot with `await send("snapshot", data)`
3. `createPollingLoop(pollFn, send, interval, eventName)` - Poll for updates
4. `createHeartbeatLoop(send, 120_000)` - Keep-alive every 2min
5. `setupSSECleanup(req.signal, close, [intervals])` - Auto-cleanup on disconnect
**Event types:**
- `snapshot` - Initial data payload
- `update` - Incremental updates
- `heartbeat` - Keep-alive ping (2min interval prevents mobile carrier timeout)
- `error` - Error event
## withAuth Middleware
```typescript
// From apps/web/src/lib/api/middleware/auth.ts
export function withAuth(handler: AuthenticatedHandler) {
return async (req: NextRequest, context: RouteContext) => {
const { userId, getToken } = await auth();
if (!userId) {
return NextResponse.json(formatErrorEntity("Authentication required"), {
status: 401,
});
}
// Get session token for Convex authentication
const sessionToken = await getToken({ template: "convex" });
if (!sessionToken) {
return NextResponse.json(
formatErrorEntity("Session token unavailable"),
{ status: 401 },
);
}
return await handler(req, { ...context, userId, sessionToken });
};
}
```
**Usage:**
- `withAuth(handler)` - Requires authentication, provides `userId` and `sessionToken`
- `withOptionalAuth(handler)` - Provides `userId?: string` if authenticated
- Always use `formatErrorEntity` for error responses
- Session token needed for `getAuthenticatedConvexClient(sessionToken)`
## Tier Selection Criteria
| Criteria | Tier 1 (Convex) | Tier 2 (SSE) | Tier 3 (REST) |
|----------|----------------|--------------|---------------|
| Platform | Web desktop | Mobile | Mobile fallback |
| Latency | <100ms real-time | ~5s updates | 30s cache |
| Duration | Unlimited | 5-30min | <30s |
| Battery | High (WebSocket) | Medium (SSE) | Low (polling) |
| Use cases | Chat messages, live lists | Progress updates, streaming | Standard CRUD |
## Key Files
- `apps/web/src/lib/utils/platform.ts` - Platform detection logic
- `apps/web/src/lib/hooks/queries/` - Hybrid data hooks
- `apps/web/src/lib/api/dal/` - DAL layer (server-only)
- `apps/web/src/app/api/v1/` - REST/SSE routes
- `apps/web/src/lib/api/sse/utils.ts` - SSE utilities
- `apps/web/src/lib/api/middleware/auth.ts` - Auth middleware
## Common Patterns
**Creating new hybrid hook:**
1. Import both React Query and Convex query hooks
2. Call `shouldUseConvex()` for platform detection
3. Conditionally enable queries with `"skip"` or `enabled: false`
4. Return unified interface
**Creating new REST endpoint:**
1. Create route in `apps/web/src/app/api/v1/{resource}/route.ts`
2. Wrap handlers with `withAuth` and `withErrorHandling`
3. Call DAL layer (never call Convex directly from routes)
4. Return `formatEntity` responses
5. Set `dynamic = "force-dynamic"`
**Creating new SSE endpoint:**
1. Create route with `/stream` suffix
2. Use `createSSEResponse()` for connection
3. Send `snapshot` event immediately
4. Setup `createPollingLoop` for updates
5. Setup `createHeartbeatLoop` (2min interval)
6. Call `setupSSECleanup` with intervals
**Adding DAL method:**
1. Create in `apps/web/src/lib/api/dal/{resource}.ts`
2. Add `import "server-only"` at top
3. Validate input with Zod schemas
4. Use `(convex.mutation as any)` + `@ts-ignore` pattern
5. Always `formatEntity` responses
6. Verify ownership before mutations
## Avoid
- Never call Convex directly from client components on mobile (use hooks)
- Never skip ownership verification in DAL mutations
- Never return raw Convex data (always use `formatEntity`)
- Don't forget `dynamic = "force-dynamic"` on API routes
- Don't skip heartbeat in SSE (mobile carriers timeout idle connections)
- Never use SSE for long operations (>30min) - use Convex actions insteadRelated Skills
MCP Architecture Expert
Design and implement Model Context Protocol servers for standardized AI-to-data integration with resources, tools, prompts, and security best practices
architecture-paradigm-pipeline
Consult this skill when designing data pipelines or transformation workflows. Use when data flows through fixed sequence of transformations, stages can be independently developed and tested, parallel processing of stages is beneficial. Do not use when selecting from multiple paradigms - use architecture-paradigms first. DO NOT use when: data flow is not sequential or predictable. DO NOT use when: complex branching/merging logic dominates.
architecture-advisor
Helps solo developers with AI agents choose optimal architecture (monolithic/microservices/hybrid)
agent-native-architecture
Build applications where agents are first-class citizens. Use this skill when designing autonomous agents, creating MCP tools, implementing self-modifying systems, or building apps where features are outcomes achieved by agents operating in a loop.
agent-architecture
Use when designing or implementing AI agent systems. Covers tool-using agents with mandatory guardrails, SSE streaming (FastAPI → Next.js via Vercel AI SDK v6), LangGraph stateful multi-agent graphs, episodic memory via pgvector, MCP overview, and production failure modes with anti-pattern/fix code pairs.
u07820-attention-management-architecture-for-personal-finance-management
Build and operate the "Attention Management Architecture for personal finance management" capability for personal finance management. Use when this exact capability is required by autonomous or human-guided missions.
account-tiering
Use when defining ABM tiers, scoring logic, and coverage rules.
MCP Server Architecture
This skill should be used when the user asks to "create an MCP server", "set up MCP server", "build ChatGPT app backend", "MCP transport type", "configure MCP endpoint", "server setup for Apps SDK", or needs guidance on MCP server architecture, transport protocols, or SDK setup for the OpenAI Apps SDK.
architecture-discipline
Use when designing/modifying system architecture or evaluating technology choices. Enforces 7-section TodoWrite with 22+ items. Triggers: "design architecture", "system design", "architectural decision", "should we use [tech]", "compare [A] vs [B]", "add new service", "microservices", "database choice", "API design", "scale to [X] users", "infrastructure decision". If thinking ANY of these, USE THIS SKILL: "quick recommendation is fine", "obvious choice", "we already know the answer", "just need to pick one", "simple architecture question".
adr-architecture
Use when documenting significant technical or architectural decisions that need context, rationale, and consequences recorded. Invoke when choosing between technology options, making infrastructure decisions, establishing standards, migrating systems, or when team needs to understand why a decision was made. Use when user mentions ADR, architecture decision, technical decision record, or decision documentation.
langchain-architecture
Design LLM applications using the LangChain framework with agents, memory, and tool integration patterns. Use when building LangChain applications, implementing AI agents, or creating complex LLM w...
architecture-agent-creation
Create specialized infrastructure agent definitions for platform/service management (Grafana, Prometheus, Traefik, ERPNext, etc.). Use when the user requests creation of an agent for a specific technology platform or infrastructure component. This skill produces complete agent prompts with integrated research, SOPs, tool references, and handoff protocols following the Linear-First Agentic Workflow framework.