backend-developer

star 2

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

pablopunk By pablopunk schedule Updated 5/1/2026

name: backend-developer description: Expert in Astro v5, Drizzle ORM, Docker Compose, OpenCode SDK v2, queue systems, and backend architecture.

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

# 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

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

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)

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:

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:

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

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

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

// 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:

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

// ❌ 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
Install via CLI
npx skills add https://github.com/pablopunk/doce.dev --skill backend-developer
Repository Details
star Stars 2
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator