# Backend Developer

> Expert in Astro v5, Drizzle ORM, Docker Compose, OpenCode SDK v2, queue systems, and backend architecture.

- Skill: `pablopunk/backend-developer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pablopunk/backend-developer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pablopunk/backend-developer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: pablopunk (https://skillmd.com/u/pablopunk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pablopunk/backend-developer

---


## Core Expertise
- Astro v5: Actions, API routes, React integration, SSR, SSE streaming
- Database: SQLite with Drizzle ORM (delegate complex DB tasks to database-expert)
- Validation: Zod schemas for inputs and payloads
- Logging: Structured JSON logging (Pino)
- Containerization: Docker Compose for multi-container orchestration
- AI Integration: OpenCode SDK v2
- Async Systems: Job queues with retries, deduplication, cancellation
- Authentication: Session-based auth with password hashing

## Use Context7 for Documentation
```bash
# Resolve library IDs, then fetch docs
ctx7 library Astro "actions API routes SSE streaming"
ctx7 docs /withastro/docs "actions API routes SSE streaming"

ctx7 library Zod "schemas validation"
ctx7 docs /colinhacks/zod "schemas validation"
```

## Astro Actions
```typescript
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';

export const server = {
  myAction: defineAction({
    input: z.object({
      projectId: z.string(),
      message: z.string(),
    }),
    handler: async (input, context) => {
      const user = context.locals.user;
      if (!user) {
        throw new ActionError({ code: "UNAUTHORIZED", message: "Must be logged in" });
      }

      const result = await doWork(input, user);
      return { success: true, data: result };
    },
  }),
};
```

**Best Practices:**
- Always use Zod schemas for input validation
- Check `context.locals.user` for auth
- Verify user owns resource
- Use ActionError codes: `UNAUTHORIZED`, `BAD_REQUEST`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_SERVER_ERROR`
- Return structured data, not raw DB rows
- For async operations, enqueue jobs without waiting (fire-and-forget)

## API Routes
```typescript
export const GET: APIRoute = async (context) => {
  const sessionToken = context.cookies.get("session")?.value;
  if (!sessionToken) return new Response(null, { status: 401 });

  const session = await validateSession(sessionToken);
  if (!session) return new Response(null, { status: 401 });

  if (session.user.id !== resource.ownerUserId) {
    return new Response(null, { status: 403 });
  }

  return Response.json(resource);
};
```

### SSE Streaming (Critical: 30s heartbeat)
```typescript
export const GET: APIRoute = async (context) => {
  return new Response(
    new ReadableStream({
      async start(controller) {
        controller.enqueue(`data: {"type": "message"}\n\n`);

        const heartbeat = setInterval(() => {
          controller.enqueue(`: keep-alive\n\n`); // Critical for WKWebView
        }, 30000);

        context.request.signal.addEventListener('abort', () => {
          clearInterval(heartbeat);
          controller.close();
        });
      },
    }),
    {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
      },
    }
  );
};
```

## Async Job Queue
**Handler Pattern:**
```typescript
interface JobContext {
  job: { id: string; type: string; payload: unknown };
  throwIfCancelRequested: () => Promise<void>;
  reschedule: (delay: number) => Promise<void>;
}

async function handleJobName(ctx: JobContext): Promise<void> {
  const payload = parsePayload(ctx.job.type, ctx.job.payload);

  await ctx.throwIfCancelRequested();
  await performOperation(payload);
  await ctx.throwIfCancelRequested();

  await enqueueNextJob({ payload });
}
```

**Key Features:**
- **Deduplication**: Jobs with same `dedupeKey` deduplicated while active
- **Retries with backoff**: Failed jobs retry (2s, 4s, 8s... up to 60s max)
- **Reschedule**: Doesn't count toward max attempts (use for polling)
- **Cooperative cancellation**: Long-running handlers call `ctx.throwIfCancelRequested()`
- **Heartbeat/Lease**: Jobs claimed for lease duration

**Polling Pattern:**
```typescript
async function handleWaitReady(ctx: JobContext): Promise<void> {
  const isReady = await checkHealthEndpoint(port);
  if (!isReady) {
    await ctx.reschedule(1000); // Wait 1s and check again
    return;
  }
  await enqueueNextJob({ projectId });
}
```

## Docker Integration
```typescript
import { composeUp, composeDown, composePs } from './docker/compose';

await composeUp(projectId, projectPath);
await composeDown(projectId, projectPath);
const containers = await composePs(projectId, projectPath);
```

**Log markers:** `[host]`, `[docker]`, `[app]`

## OpenCode SDK v2
```typescript
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";

const clientCache = new Map<number, OpencodeClient>();

function getOpencodeClient(port: number): OpencodeClient {
  if (!clientCache.has(port)) {
    clientCache.set(port, createOpencodeClient({ baseUrl: `http://127.0.0.1:${port}` }));
  }
  return clientCache.get(port)!;
}

// Session management
const session = await client.session.create({ body: { title: "Session", model: "provider:model" } });
await client.session.messages({ path: { id: sessionId }, body: { parts: [{ type: "text", text: "prompt" }] } });
```

**Normalized Event Types:**
- `chat.session.status`, `chat.message.part.added`, `chat.message.final`
- `chat.tool.start`, `chat.tool.finish`, `chat.tool.error`
- `chat.reasoning.part`, `chat.file.changed`

## Error Handling
```typescript
// Type-safe error handling
catch (error) {
  logger.error({ error: error instanceof Error ? error.message : String(error) }, "Operation failed");
}

// Critical vs non-critical
// Critical - rethrow
try { await criticalOperation(); } catch (error) { logger.error({ error }); throw error; }

// Non-critical - log and continue
try { await nonCriticalOperation(); } catch (error) { logger.warn({ error }); return null; }
```

**Log with context:**
```typescript
logger.error({ error, userId, resourceId, operation: "createResource" }, "Failed to create resource");
```

## Clean Code Principles
- Separate domains with folders
- Use abstractions (DB operations in models, Docker in compose module)
- Functions have ONE purpose only
- Functions declare WHAT, not HOW (call smaller functions)

## Common Pitfalls
```typescript
// ❌ Fire-and-forget without error handler
void backgroundTask();

// ✅ Correct
void backgroundTask().catch(error => logger.error({ error }, "Background task failed"));

// ❌ Not clearing intervals
useEffect(() => { const id = setInterval(poll, 5000); }, []);

// ✅ Correct
useEffect(() => { const id = setInterval(poll, 5000); return () => clearInterval(id); }, []);

// ❌ Missing await
handler: async () => { initializeSession(); return { success: true }; }

// ✅ Correct
handler: async () => { await initializeSession(); return { success: true }; };
```

## Best Practices
1. Always use ctx7 for Astro, Zod, and other library docs
2. Delegate complex DB tasks (schema, optimization, migrations) to database-expert
3. Keep transactions short, use WAL mode
4. Validate inputs with Zod, check auth/ownership
5. Log with context, use structured error handling
6. Implement 30s heartbeat for SSE
7. Cache OpenCode clients by port
8. Use fire-and-forget pattern with .catch() for background tasks
9. Test critical paths, verify security

