backend-systems-protocols
SKILL.md
Backend Systems Protocols Skill
Overview
Database design conventions, API contract definitions, authentication patterns, and error handling standards for backend development. This skill provides the domain knowledge for building consistent, well-structured backend systems.
Type
standards / domain-knowledge
When to Use
Trigger this skill when:
- Designing database schemas
- Defining API contracts and endpoints
- Implementing authentication/authorization
- Standardizing error handling
- Setting up backend conventions for a project
Keywords: database, schema, API, REST, GraphQL, contract, authentication, auth, JWT, session, error handling, backend, server, endpoints
Database Conventions
ID Strategy
-- Decision: UUID for all primary keys
-- Rationale: Frontend-safe generation, no sequential exposure, merge-friendly
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Example
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- ...
);
Alternatives and when to use:
| Strategy | Use When |
|---|---|
| UUID | Default choice, distributed systems, frontend ID generation |
| ULID | Need sortable IDs with timestamp component |
| Auto-increment | Internal-only tables, legacy compatibility |
Timestamp Conventions
-- Always include created_at and updated_at
-- Always use timestamptz (timezone-aware)
-- Auto-update via trigger
CREATE TABLE example (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- ... other columns ...
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Auto-update trigger function (create once)
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply to each table
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON example
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
Soft Delete Pattern
-- Add deleted_at column for soft delete
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) NOT NULL,
-- ...
deleted_at TIMESTAMPTZ, -- NULL = active, timestamp = deleted
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Partial unique index (only active records)
CREATE UNIQUE INDEX users_email_unique
ON users (email)
WHERE deleted_at IS NULL;
-- Default query scope
-- SELECT * FROM users WHERE deleted_at IS NULL;
When to use:
| Approach | Use Case |
|---|---|
| Soft delete | User data, audit requirements, undo capability |
| Hard delete | Logs, sessions, temporary data |
Naming Conventions
-- Tables: plural, snake_case
CREATE TABLE user_profiles (...);
CREATE TABLE order_items (...);
-- Columns: snake_case
user_id, created_at, is_active
-- Foreign keys: singular_table_id
user_id REFERENCES users(id)
order_id REFERENCES orders(id)
-- Indexes: table_column_idx
CREATE INDEX users_email_idx ON users(email);
-- Constraints: table_column_type
CONSTRAINT users_email_unique UNIQUE (email)
CONSTRAINT orders_total_positive CHECK (total >= 0)
API Contract Definitions
Response Wrapper Pattern
// Standard success response
interface ApiResponse<T> {
success: true;
data: T;
meta?: {
requestId: string;
timestamp: string;
};
}
// Standard error response
interface ApiError {
success: false;
error: {
code: string; // Machine-readable: "USER_NOT_FOUND"
message: string; // Human-readable: "User not found"
details?: Record<string, string[]>; // Field-level validation errors
};
meta?: {
requestId: string;
timestamp: string;
};
}
// Union type for handlers
type ApiResult<T> = ApiResponse<T> | ApiError;
Pagination Pattern
interface PaginatedResponse<T> {
items: T[];
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
}
// Request params
interface PaginationParams {
page?: number; // Default: 1
pageSize?: number; // Default: 20, Max: 100
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
// Usage: GET /api/users?page=2&pageSize=20&sortBy=createdAt&sortOrder=desc
Resource Representation
// Database model (internal)
interface UserRecord {
id: string;
email: string;
password_hash: string; // Never exposed
display_name: string;
avatar_url: string | null;
created_at: Date;
updated_at: Date;
deleted_at: Date | null;
}
// API representation (external)
interface User {
id: string;
email: string;
displayName: string; // camelCase transformation
avatarUrl: string | null;
createdAt: string; // ISO 8601 string
updatedAt: string;
}
// Create request (what client sends)
interface UserCreateRequest {
email: string;
password: string;
displayName: string;
}
// Update request (partial)
interface UserUpdateRequest {
displayName?: string;
avatarUrl?: string | null;
}
Naming Transformation
Database (snake_case) → API (camelCase)
user_id → userId
created_at → createdAt
display_name → displayName
is_active → isActive
Transform at the boundary - database layer uses snake_case, API layer uses camelCase.
Error Handling
Standard Error Codes
const ERROR_CODES = {
// Validation (400)
VALIDATION_ERROR: 'VALIDATION_ERROR',
INVALID_INPUT: 'INVALID_INPUT',
MISSING_FIELD: 'MISSING_FIELD',
// Authentication (401)
UNAUTHORIZED: 'UNAUTHORIZED',
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
TOKEN_INVALID: 'TOKEN_INVALID',
// Authorization (403)
FORBIDDEN: 'FORBIDDEN',
INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
// Resources (404, 409)
NOT_FOUND: 'NOT_FOUND',
ALREADY_EXISTS: 'ALREADY_EXISTS',
CONFLICT: 'CONFLICT',
// Rate limiting (429)
RATE_LIMITED: 'RATE_LIMITED',
// Server (500, 503)
INTERNAL_ERROR: 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
DATABASE_ERROR: 'DATABASE_ERROR',
} as const;
// HTTP status mapping
const ERROR_STATUS: Record<string, number> = {
VALIDATION_ERROR: 400,
INVALID_INPUT: 400,
MISSING_FIELD: 400,
UNAUTHORIZED: 401,
INVALID_CREDENTIALS: 401,
TOKEN_EXPIRED: 401,
TOKEN_INVALID: 401,
FORBIDDEN: 403,
INSUFFICIENT_PERMISSIONS: 403,
NOT_FOUND: 404,
ALREADY_EXISTS: 409,
CONFLICT: 409,
RATE_LIMITED: 429,
INTERNAL_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
DATABASE_ERROR: 500,
};
Validation Error Format
// Field-level validation errors
{
success: false,
error: {
code: "VALIDATION_ERROR",
message: "Validation failed",
details: {
email: ["Invalid email format", "Email already exists"],
password: ["Must be at least 8 characters"]
}
}
}
Authentication Patterns
JWT in HttpOnly Cookies (Recommended)
┌─────────────────────────────────────────────────────────────┐
│ AUTH FLOW │
├─────────────────────────────────────────────────────────────┤
│ Access token: 15 min expiry, in httpOnly cookie │
│ Refresh token: 7 day expiry, rotates on use │
│ CSRF: Double-submit cookie pattern │
├─────────────────────────────────────────────────────────────┤
│ Endpoints: │
│ POST /api/auth/login │
│ → Sets access_token and refresh_token cookies │
│ → Returns user object │
│ │
│ POST /api/auth/logout │
│ → Clears all auth cookies │
│ → Invalidates refresh token in DB │
│ │
│ POST /api/auth/refresh │
│ → Validates refresh token │
│ → Issues new access + refresh tokens │
│ → Rotates refresh token (old one invalidated) │
│ │
│ GET /api/auth/me │
│ → Returns current user or 401 │
└─────────────────────────────────────────────────────────────┘
Cookie Configuration
// Access token cookie
{
name: 'access_token',
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // or 'lax' for OAuth redirects
path: '/',
maxAge: 15 * 60, // 15 minutes
}
// Refresh token cookie
{
name: 'refresh_token',
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/api/auth', // Only sent to auth endpoints
maxAge: 7 * 24 * 60 * 60, // 7 days
}
CSRF Protection
// Double-submit cookie pattern
// 1. Server sets CSRF token in non-httpOnly cookie
// 2. Client reads cookie, sends in X-CSRF-Token header
// 3. Server validates header matches cookie
// Set on login
res.cookie('csrf_token', generateCsrfToken(), {
httpOnly: false, // JS must read this
secure: true,
sameSite: 'strict',
});
// Validate on state-changing requests
const headerToken = req.headers['x-csrf-token'];
const cookieToken = req.cookies.csrf_token;
if (headerToken !== cookieToken) {
throw new ForbiddenError('CSRF validation failed');
}
State Handling Matrix
Define how frontend should handle each state:
| State | Backend Response | HTTP Status | Frontend Behavior |
|---|---|---|---|
| Loading | N/A (pending) | - | Show skeleton/spinner |
| Success (data) | { success: true, data: [...] } |
200 | Render data |
| Success (empty) | { success: true, data: [] } |
200 | Show empty state |
| Created | { success: true, data: {...} } |
201 | Show success, redirect |
| No Content | - | 204 | Silent success |
| Bad Request | { success: false, error: {...} } |
400 | Show validation errors |
| Unauthorized | { success: false, error: {...} } |
401 | Redirect to login |
| Forbidden | { success: false, error: {...} } |
403 | Show permission error |
| Not Found | { success: false, error: {...} } |
404 | Show not found state |
| Server Error | { success: false, error: {...} } |
500 | Show error + retry |
| Offline | No response | - | Show cached + banner |
Endpoint Design
RESTful Conventions
Resources are nouns, actions are HTTP methods
GET /api/users → List users
POST /api/users → Create user
GET /api/users/:id → Get single user
PATCH /api/users/:id → Partial update
PUT /api/users/:id → Full replace (rare)
DELETE /api/users/:id → Delete user
Nested resources
GET /api/users/:id/posts → User's posts
POST /api/users/:id/posts → Create post for user
Actions (when REST doesn't fit)
POST /api/users/:id/activate → Custom action
POST /api/auth/login → Authentication
POST /api/payments/process → Complex operation
Request/Response Examples
// GET /api/users/:id
// Response: 200 OK
{
success: true,
data: {
id: "550e8400-e29b-41d4-a716-446655440000",
email: "user@example.com",
displayName: "John Doe",
avatarUrl: null,
createdAt: "2024-01-15T10:30:00.000Z",
updatedAt: "2024-01-15T10:30:00.000Z"
}
}
// POST /api/users
// Request body:
{
email: "user@example.com",
password: "securepassword123",
displayName: "John Doe"
}
// Response: 201 Created
{
success: true,
data: {
id: "550e8400-e29b-41d4-a716-446655440000",
email: "user@example.com",
displayName: "John Doe",
avatarUrl: null,
createdAt: "2024-01-15T10:30:00.000Z",
updatedAt: "2024-01-15T10:30:00.000Z"
}
}
// PATCH /api/users/:id
// Request body:
{
displayName: "Jane Doe"
}
// Response: 200 OK
{
success: true,
data: { /* updated user */ }
}
// DELETE /api/users/:id
// Response: 204 No Content
Pre-Implementation Checklist
- Database naming conventions documented
- ID strategy decided (UUID recommended)
- Timestamp handling specified
- Soft delete vs hard delete decided per entity
- API response wrapper types defined
- Error codes enumerated
- Pagination pattern specified
- Authentication flow documented
- CSRF protection planned
Anti-Patterns
NEVER do these:
- Expose database column names directly in API (snake_case leak)
- Return password hashes or other secrets
- Use auto-increment IDs in public APIs
- Inconsistent error response shapes
- Store JWT in localStorage (XSS vulnerable)
- Skip CSRF protection on state-changing requests
- Return 200 OK for errors (use proper status codes)
- Inconsistent timestamp formats (always ISO 8601)
Standalone skill for backend system protocols
Weekly Installs
1
Repository
seqis/openclaw-…ude-codeGitHub Stars
31
First Seen
12 days ago
Security Audits
Installed on
amp1
cline1
openclaw1
opencode1
cursor1
kimi-cli1