Claim this agent
Agent DossierCLAWHUBSafety 84/100

Xpersona Agent

afrexai-nodejs-production

Node.js & TypeScript Production Engineering Node.js & TypeScript Production Engineering Complete methodology for building production-grade Node.js backends with TypeScript. Covers architecture, frameworks, error handling, database patterns, security, testing, observability, and deployment. --- Quick Health Check (/16) Run through these 8 signals — score 0 (missing) or 2 (present): | # | Signal | Check | |---|--------|-------| | 1 | Strict TypeScript | "strict"

OpenClaw · self-declared
Trust evidence available
clawhub skill install skills:1kalin:afrexai-nodejs-production

Overall rank

#62

Adoption

No public adoption signal

Trust

Unknown

Freshness

Feb 25, 2026

Freshness

Last checked Feb 25, 2026

Best For

afrexai-nodejs-production is best for both workflows where OpenClaw compatibility matters.

Not Ideal For

Contract metadata is missing or unavailable for deterministic execution.

Evidence Sources Checked

editorial-content, CLAWHUB, runtime-metrics, public facts pack

Overview

Key links, install path, reliability highlights, and the shortest practical read before diving into the crawl record.

Verifiededitorial-content

Overview

Executive Summary

Node.js & TypeScript Production Engineering Node.js & TypeScript Production Engineering Complete methodology for building production-grade Node.js backends with TypeScript. Covers architecture, frameworks, error handling, database patterns, security, testing, observability, and deployment. --- Quick Health Check (/16) Run through these 8 signals — score 0 (missing) or 2 (present): | # | Signal | Check | |---|--------|-------| | 1 | Strict TypeScript | "strict" Capability contract not published. No trust telemetry is available yet. Last updated 4/15/2026.

No verified compatibility signals

Trust score

Unknown

Compatibility

OpenClaw

Freshness

Feb 25, 2026

Vendor

Openclaw

Artifacts

0

Benchmarks

0

Last release

Unpublished

Install & run

Setup Snapshot

clawhub skill install skills:1kalin:afrexai-nodejs-production
  1. 1

    Setup complexity is LOW. This package is likely designed for quick installation with minimal external side-effects.

  2. 2

    Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data.

Evidence & Timeline

Public facts grouped by evidence type, plus release and crawl events with provenance and freshness.

Verifiededitorial-content

Public facts

Evidence Ledger

Vendor (1)

Vendor

Openclaw

profilemedium
Observed Apr 15, 2026Source linkProvenance
Compatibility (1)

Protocol compatibility

OpenClaw

contractmedium
Observed Apr 15, 2026Source linkProvenance
Security (1)

Handshake status

UNKNOWN

trustmedium
Observed unknownSource linkProvenance
Integration (1)

Crawlable docs

6 indexed pages on the official domain

search_documentmedium
Observed Apr 15, 2026Source linkProvenance

Artifacts & Docs

Parameters, dependencies, examples, extracted files, editorial overview, and the complete README when available.

Self-declaredCLAWHUB

Captured outputs

Artifacts Archive

Extracted files

0

Examples

6

Snippets

0

Languages

typescript

Parameters

Executable Examples

text

src/
├── index.ts              # Entry point — bootstrap only
├── app.ts                # App factory (createApp)
├── config/
│   ├── env.ts            # Environment validation (Zod)
│   └── database.ts       # DB connection config
├── routes/
│   ├── index.ts          # Route registry
│   ├── users.ts          # /users routes
│   └── orders.ts         # /orders routes
├── services/
│   ├── user.service.ts   # Business logic
│   └── order.service.ts
├── repositories/
│   ├── user.repo.ts      # Data access (Drizzle/Prisma)
│   └── order.repo.ts
├── middleware/
│   ├── auth.ts           # Authentication
│   ├── validate.ts       # Request validation
│   ├── error-handler.ts  # Global error handler
│   └── request-id.ts     # Correlation ID
├── errors/
│   └── index.ts          # Custom error classes
├── types/
│   └── index.ts          # Shared types
├── utils/
│   └── index.ts          # Pure utility functions
├── jobs/                 # Background jobs/queues
│   └── email.job.ts
└── __tests__/            # Tests mirror src/ structure
    ├── services/
    └── routes/
drizzle/                  # Database migrations
├── 0001_create_users.sql
└── meta/
tsconfig.json
package.json
Dockerfile
docker-compose.yml
.env.example

json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

typescript

// types/branded.ts
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { [__brand]: B };

export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
export type Email = Brand<string, 'Email'>;

export function UserId(id: string): UserId { return id as UserId; }
export function Email(email: string): Email {
  if (!email.includes('@')) throw new ValidationError('Invalid email');
  return email as Email;
}

typescript

// config/env.ts
import { z } from 'zod';

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url().optional(),
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default('15m'),
  LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
  CORS_ORIGINS: z.string().transform(s => s.split(',')).default('*'),
  RATE_LIMIT_MAX: z.coerce.number().default(100),
  RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
});

export type Env = z.infer<typeof EnvSchema>;

// Validate at import time — fail fast
const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
  console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);
  process.exit(1);
}

export const env = parsed.data;

typescript

// errors/index.ts
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly details?: Record<string, unknown>,
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends AppError {
  constructor(message: string, details?: Record<string, unknown>) {
    super(message, 'VALIDATION_ERROR', 400, details);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404, { resource, id });
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 'UNAUTHORIZED', 401);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Insufficient permissions') {
    super(message, 'FORBIDDEN', 403);
  }
}

export class ConflictError extends AppError {
  constructor(message: string, details?: Record<string, unknown>) {
    super(message, 'CONFLICT', 409, details);
  }
}

export class RateLimitError extends AppError {
  constructor(retryAfterMs: number) {
    super('Too many requests', 'RATE_LIMITED', 429, { retryAfterMs });
  }
}

typescript

// middleware/error-handler.ts
import type { ErrorHandler } from 'hono';
import { AppError } from '../errors/index.js';
import { logger } from '../utils/logger.js';

export const errorHandler: ErrorHandler = (err, c) => {
  const requestId = c.get('requestId');

  if (err instanceof AppError) {
    if (err.statusCode >= 500) {
      logger.error({ err, requestId }, 'Server error');
    } else {
      logger.warn({ err, requestId }, 'Client error');
    }

    return c.json({
      error: { code: err.code, message: err.message, details: err.details },
    }, err.statusCode as any);
  }

  // Unexpected errors — don't leak internals
  logger.error({ err, requestId }, 'Unhandled error');
  return c.json({
    error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' },
  }, 500);
};

Editorial read

Docs & README

Docs source

CLAWHUB

Editorial quality

ready

Node.js & TypeScript Production Engineering Node.js & TypeScript Production Engineering Complete methodology for building production-grade Node.js backends with TypeScript. Covers architecture, frameworks, error handling, database patterns, security, testing, observability, and deployment. --- Quick Health Check (/16) Run through these 8 signals — score 0 (missing) or 2 (present): | # | Signal | Check | |---|--------|-------| | 1 | Strict TypeScript | "strict"

Full README

Node.js & TypeScript Production Engineering

Complete methodology for building production-grade Node.js backends with TypeScript. Covers architecture, frameworks, error handling, database patterns, security, testing, observability, and deployment.


Quick Health Check (/16)

Run through these 8 signals — score 0 (missing) or 2 (present):

| # | Signal | Check | |---|--------|-------| | 1 | Strict TypeScript | "strict": true in tsconfig, no any escape hatches | | 2 | Structured errors | Custom error classes with codes, not string throws | | 3 | Input validation | Zod/Valibot on every external boundary | | 4 | Database migrations | Version-controlled, reversible, CI-enforced | | 5 | Health endpoints | /health (liveness) + /ready (readiness) with dependency checks | | 6 | Structured logging | JSON logs with request IDs, no console.log in prod | | 7 | Test coverage | >80% unit, integration tests for critical paths | | 8 | Graceful shutdown | SIGTERM handler drains connections before exit |

Score interpretation: 0-6 = critical gaps, 8-10 = needs work, 12-14 = solid, 16 = production-grade.


Phase 1: Architecture & Project Structure

Framework Selection Matrix

| Framework | Best For | Throughput | Ecosystem | Learning Curve | TypeScript | |-----------|----------|-----------|-----------|----------------|------------| | Hono | Edge/serverless, lightweight APIs | ⭐⭐⭐⭐⭐ | Growing fast | Low | Native | | Fastify | High-perf monoliths, JSON APIs | ⭐⭐⭐⭐⭐ | Mature | Medium | Excellent | | Express | Legacy, max middleware ecosystem | ⭐⭐⭐ | Massive | Low | Via @types | | NestJS | Enterprise, large teams, DI-heavy | ⭐⭐⭐⭐ | Large | High | Native | | Elysia | Bun-first, type-safe APIs | ⭐⭐⭐⭐⭐ | Small | Low | Native | | tRPC | Full-stack TS, type-safe RPC | N/A (layer) | Growing | Medium | Native |

Decision rules:

  • New project, small team → Hono (portable, fast, minimal)
  • High throughput JSON API → Fastify (proven, benchmarked)
  • Enterprise, 10+ developers → NestJS (structure enforced)
  • Full-stack TypeScript monorepo → tRPC (end-to-end types)
  • Existing Express codebase → Stay on Express, migrate incrementally
  • Bun runtime → Elysia or Hono

Recommended Project Structure

src/
├── index.ts              # Entry point — bootstrap only
├── app.ts                # App factory (createApp)
├── config/
│   ├── env.ts            # Environment validation (Zod)
│   └── database.ts       # DB connection config
├── routes/
│   ├── index.ts          # Route registry
│   ├── users.ts          # /users routes
│   └── orders.ts         # /orders routes
├── services/
│   ├── user.service.ts   # Business logic
│   └── order.service.ts
├── repositories/
│   ├── user.repo.ts      # Data access (Drizzle/Prisma)
│   └── order.repo.ts
├── middleware/
│   ├── auth.ts           # Authentication
│   ├── validate.ts       # Request validation
│   ├── error-handler.ts  # Global error handler
│   └── request-id.ts     # Correlation ID
├── errors/
│   └── index.ts          # Custom error classes
├── types/
│   └── index.ts          # Shared types
├── utils/
│   └── index.ts          # Pure utility functions
├── jobs/                 # Background jobs/queues
│   └── email.job.ts
└── __tests__/            # Tests mirror src/ structure
    ├── services/
    └── routes/
drizzle/                  # Database migrations
├── 0001_create_users.sql
└── meta/
tsconfig.json
package.json
Dockerfile
docker-compose.yml
.env.example

7 Architecture Rules

  1. Routes → Services → Repositories — never skip layers
  2. Services contain business logic — routes are thin (validate + call service + respond)
  3. Repositories own data access — services never import DB client directly
  4. No circular dependencies — dependency flow is strictly downward
  5. One export per file — makes imports predictable, testing easy
  6. Config validated at startup — fail fast, not at runtime
  7. ≤50 lines per function, ≤300 lines per file — split when exceeded

Phase 2: TypeScript Configuration

Production tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

8 TypeScript Rules

  1. Never use any — use unknown and narrow, or define proper types
  2. noUncheckedIndexedAccess: true — forces null checks on array/object access
  3. Branded types for IDstype UserId = string & { __brand: 'UserId' }
  4. Zod schemas derive typestype User = z.infer<typeof UserSchema>
  5. Discriminated unions for states{ status: 'active'; data: T } | { status: 'error'; error: E }
  6. satisfies over as — preserves narrowed types: config satisfies Config
  7. Enums → const objectsconst Status = { ACTIVE: 'active', INACTIVE: 'inactive' } as const
  8. Return types on public APIs — explicit return types on exported functions

Branded Types Pattern

// types/branded.ts
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { [__brand]: B };

export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
export type Email = Brand<string, 'Email'>;

export function UserId(id: string): UserId { return id as UserId; }
export function Email(email: string): Email {
  if (!email.includes('@')) throw new ValidationError('Invalid email');
  return email as Email;
}

Phase 3: Environment & Configuration

Validated Configuration with Zod

// config/env.ts
import { z } from 'zod';

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url().optional(),
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default('15m'),
  LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
  CORS_ORIGINS: z.string().transform(s => s.split(',')).default('*'),
  RATE_LIMIT_MAX: z.coerce.number().default(100),
  RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
});

export type Env = z.infer<typeof EnvSchema>;

// Validate at import time — fail fast
const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
  console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);
  process.exit(1);
}

export const env = parsed.data;

5 Configuration Rules

  1. Validate ALL env vars at startup — never read process.env directly elsewhere
  2. Type-safe access only — import env object, not process.env
  3. Secrets via vault/env — never hardcoded, never committed
  4. .env.example always current — document every var with description + default
  5. Different configs per environment — use NODE_ENV for branching, never feature flags in env

Phase 4: Error Handling Architecture

Custom Error Hierarchy

// errors/index.ts
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly details?: Record<string, unknown>,
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends AppError {
  constructor(message: string, details?: Record<string, unknown>) {
    super(message, 'VALIDATION_ERROR', 400, details);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404, { resource, id });
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 'UNAUTHORIZED', 401);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Insufficient permissions') {
    super(message, 'FORBIDDEN', 403);
  }
}

export class ConflictError extends AppError {
  constructor(message: string, details?: Record<string, unknown>) {
    super(message, 'CONFLICT', 409, details);
  }
}

export class RateLimitError extends AppError {
  constructor(retryAfterMs: number) {
    super('Too many requests', 'RATE_LIMITED', 429, { retryAfterMs });
  }
}

Global Error Handler

// middleware/error-handler.ts
import type { ErrorHandler } from 'hono';
import { AppError } from '../errors/index.js';
import { logger } from '../utils/logger.js';

export const errorHandler: ErrorHandler = (err, c) => {
  const requestId = c.get('requestId');

  if (err instanceof AppError) {
    if (err.statusCode >= 500) {
      logger.error({ err, requestId }, 'Server error');
    } else {
      logger.warn({ err, requestId }, 'Client error');
    }

    return c.json({
      error: { code: err.code, message: err.message, details: err.details },
    }, err.statusCode as any);
  }

  // Unexpected errors — don't leak internals
  logger.error({ err, requestId }, 'Unhandled error');
  return c.json({
    error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' },
  }, 500);
};

6 Error Handling Rules

  1. Throw custom errors, catch at boundary — services throw, routes/middleware catch
  2. Never throw strings — always throw new SomeError(message)
  3. Don't leak internals — 5xx returns generic message, log the real error
  4. Include error codes — machine-readable codes for client handling
  5. Validate early, fail fast — Zod at route level before business logic
  6. Async errors auto-caught — use frameworks that handle rejected promises (Hono/Fastify do this natively)

Phase 5: Input Validation

Zod Schema Patterns

// routes/users.ts
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';

const CreateUserSchema = z.object({
  email: z.string().email().max(255).toLowerCase(),
  name: z.string().min(1).max(100).trim(),
  role: z.enum(['user', 'admin']).default('user'),
});

const PaginationSchema = z.object({
  cursor: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

const UserIdParamSchema = z.object({
  id: z.string().uuid(),
});

// Usage with Hono
app.post('/users', zValidator('json', CreateUserSchema), async (c) => {
  const body = c.req.valid('json'); // Fully typed!
  const user = await userService.create(body);
  return c.json({ data: user }, 201);
});

app.get('/users', zValidator('query', PaginationSchema), async (c) => {
  const { cursor, limit } = c.req.valid('query');
  const result = await userService.list({ cursor, limit });
  return c.json({ data: result.items, nextCursor: result.nextCursor });
});

Validation Rules

  1. Validate at the edge — every route handler validates input before calling services
  2. Schemas define the contract — derive TypeScript types from Zod, not the other way around
  3. Transform in schemas.trim(), .toLowerCase(), .default() in Zod, not in services
  4. Reuse common schemasPaginationSchema, DateRangeSchema, SortSchema
  5. Validate path params too — UUIDs, slugs, numeric IDs

Phase 6: Database Patterns

ORM Selection Guide

| ORM | Best For | Type Safety | Migration | Query Builder | Learning Curve | |-----|----------|-------------|-----------|---------------|----------------| | Drizzle | SQL-first, edge, performance | ⭐⭐⭐⭐⭐ | Built-in | SQL-like | Low | | Prisma | Rapid dev, schema-first | ⭐⭐⭐⭐ | Built-in | Custom DSL | Low | | Kysely | SQL purists, complex queries | ⭐⭐⭐⭐⭐ | External | Type-safe SQL | Medium | | TypeORM | Legacy, decorator-style | ⭐⭐⭐ | Built-in | ORM-style | Medium |

Decision: Drizzle for new projects (matches SQL mental model, best TypeScript inference, edge-compatible).

Drizzle Schema Example

// drizzle/schema.ts
import { pgTable, text, timestamp, uuid, varchar, boolean, index } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }).notNull(),
  role: text('role', { enum: ['user', 'admin'] }).notNull().default('user'),
  isActive: boolean('is_active').notNull().default(true),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
  index('idx_users_email').on(table.email),
  index('idx_users_created').on(table.createdAt),
]);

Repository Pattern

// repositories/user.repo.ts
import { eq, desc, gt } from 'drizzle-orm';
import { db } from '../config/database.js';
import { users } from '../../drizzle/schema.js';
import type { UserId } from '../types/branded.js';
import { NotFoundError } from '../errors/index.js';

export class UserRepository {
  async findById(id: UserId) {
    const [user] = await db.select().from(users).where(eq(users.id, id)).limit(1);
    if (!user) throw new NotFoundError('User', id);
    return user;
  }

  async list(opts: { cursor?: string; limit: number }) {
    const query = db.select().from(users).orderBy(desc(users.createdAt)).limit(opts.limit + 1);
    if (opts.cursor) query.where(gt(users.id, opts.cursor));
    const rows = await query;
    const hasMore = rows.length > opts.limit;
    return { items: rows.slice(0, opts.limit), nextCursor: hasMore ? rows[opts.limit - 1].id : undefined };
  }

  async create(data: { email: string; name: string; role?: string }) {
    const [user] = await db.insert(users).values(data).returning();
    return user;
  }

  async update(id: UserId, data: Partial<{ name: string; role: string; isActive: boolean }>) {
    const [user] = await db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, id)).returning();
    if (!user) throw new NotFoundError('User', id);
    return user;
  }
}

Database Rules

  1. Cursor-based pagination — never OFFSET for user-facing lists
  2. Migrations in version controldrizzle-kit generate → commit → drizzle-kit migrate in CI
  3. Connection pooling — use pg pool (min: 2, max: 10 per process) or serverless driver for edge
  4. Transactions for multi-writedb.transaction(async (tx) => { ... })
  5. Index query patterns — add indexes for WHERE + ORDER BY columns, monitor slow queries
  6. SQLite for dev, Postgres for prod — Drizzle supports both with same schema (use better-sqlite3 locally)

Phase 7: Authentication & Authorization

JWT Auth Middleware (Hono)

// middleware/auth.ts
import { jwt } from 'hono/jwt';
import { env } from '../config/env.js';
import type { UserId } from '../types/branded.js';

type JwtPayload = { sub: UserId; role: 'user' | 'admin'; iat: number; exp: number };

export const authenticate = jwt({ secret: env.JWT_SECRET });

export const requireRole = (...roles: string[]) => {
  return async (c: any, next: any) => {
    const payload = c.get('jwtPayload') as JwtPayload;
    if (!roles.includes(payload.role)) {
      throw new ForbiddenError(`Required role: ${roles.join(' or ')}`);
    }
    await next();
  };
};

Security Checklist

| # | Item | Priority | |---|------|----------| | 1 | Helmet/security headers (CSP, HSTS, X-Frame) | P0 | | 2 | Rate limiting per IP + per user | P0 | | 3 | Input validation on every endpoint | P0 | | 4 | CORS configured (not * in prod) | P0 | | 5 | JWT short-lived (15m) + refresh token rotation | P0 | | 6 | Password hashing (argon2id, cost ≥ 3) | P0 | | 7 | SQL injection prevention (parameterized queries) | P0 | | 8 | Request size limits (1MB default) | P1 | | 9 | Dependency audit (npm audit, Snyk) | P1 | | 10 | API key scoping (read-only, write, admin) | P1 |


Phase 8: Structured Logging & Observability

Pino Logger Setup

// utils/logger.ts
import pino from 'pino';
import { env } from '../config/env.js';

export const logger = pino({
  level: env.LOG_LEVEL,
  ...(env.NODE_ENV === 'development' && { transport: { target: 'pino-pretty' } }),
  serializers: { err: pino.stdSerializers.err },
  redact: ['req.headers.authorization', '*.password', '*.token', '*.secret'],
  formatters: {
    level: (label) => ({ level: label }),
  },
});

Request ID Middleware

// middleware/request-id.ts
import { randomUUID } from 'node:crypto';

export const requestId = () => {
  return async (c: any, next: any) => {
    const id = c.req.header('x-request-id') || randomUUID();
    c.set('requestId', id);
    c.header('x-request-id', id);
    await next();
  };
};

Request Logging Middleware

// middleware/request-logger.ts
import { logger } from '../utils/logger.js';

export const requestLogger = () => {
  return async (c: any, next: any) => {
    const start = performance.now();
    const requestId = c.get('requestId');

    await next();

    const duration = Math.round(performance.now() - start);
    const level = c.res.status >= 500 ? 'error' : c.res.status >= 400 ? 'warn' : 'info';

    logger[level]({
      requestId,
      method: c.req.method,
      path: c.req.path,
      status: c.res.status,
      durationMs: duration,
      userAgent: c.req.header('user-agent'),
    }, `${c.req.method} ${c.req.path} ${c.res.status} ${duration}ms`);
  };
};

Health Endpoints

// routes/health.ts
app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }));

app.get('/ready', async (c) => {
  const checks = {
    database: false,
    redis: false,
  };

  try {
    await db.execute(sql`SELECT 1`);
    checks.database = true;
  } catch {}

  try {
    await redis.ping();
    checks.redis = true;
  } catch {}

  const ready = Object.values(checks).every(Boolean);
  return c.json({ status: ready ? 'ready' : 'degraded', checks }, ready ? 200 : 503);
});

Phase 9: Testing Strategy

Test Pyramid

| Level | Coverage Target | Tools | What to Test | |-------|----------------|-------|-------------| | Unit | >80% | Vitest | Services, utils, pure logic | | Integration | Critical paths | Vitest + testcontainers | Routes → DB round-trip | | E2E | Happy paths | Vitest + supertest | Full HTTP request cycle | | Contract | API boundaries | Vitest | Request/response shapes |

Vitest Configuration

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts'],
    coverage: { provider: 'v8', reporter: ['text', 'lcov'], thresholds: { lines: 80, branches: 75 } },
    setupFiles: ['./src/__tests__/setup.ts'],
    pool: 'forks', // Isolation for DB tests
  },
});

Service Unit Test Pattern

// __tests__/services/user.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from '../../services/user.service.js';
import type { UserRepository } from '../../repositories/user.repo.js';

describe('UserService', () => {
  let service: UserService;
  let repo: jest.Mocked<UserRepository>;

  beforeEach(() => {
    repo = { findById: vi.fn(), create: vi.fn(), update: vi.fn(), list: vi.fn() } as any;
    service = new UserService(repo);
  });

  it('creates user with normalized email', async () => {
    repo.create.mockResolvedValue({ id: '1', email: 'test@example.com', name: 'Test' } as any);
    const result = await service.create({ email: 'Test@Example.COM', name: 'Test' });
    expect(repo.create).toHaveBeenCalledWith(expect.objectContaining({ email: 'test@example.com' }));
    expect(result.email).toBe('test@example.com');
  });
});

Integration Test Pattern

// __tests__/routes/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createApp } from '../../app.js';
import { testDb, migrate, cleanup } from '../helpers/db.js';

describe('POST /users', () => {
  let app: any;

  beforeAll(async () => {
    await migrate(testDb);
    app = createApp({ db: testDb });
  });

  afterAll(async () => { await cleanup(testDb); });

  it('creates user and returns 201', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'new@example.com', name: 'New User' }),
    });

    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.data.email).toBe('new@example.com');
  });

  it('rejects invalid email with 400', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'not-an-email', name: 'Bad' }),
    });

    expect(res.status).toBe(400);
  });
});

7 Testing Rules

  1. Test behavior, not implementation — assert outputs, not internal method calls
  2. Each test is independent — no shared mutable state between tests
  3. Name tests as specificationsit('rejects expired JWT with 401')
  4. Fast unit tests, isolated integration tests — use pool: 'forks' for DB tests
  5. Mock at boundaries — mock repositories in service tests, real DB in integration tests
  6. Test error paths — 400s, 404s, 409s, 500s, not just happy paths
  7. CI enforces coverage — fail build if coverage drops below threshold

Phase 10: Graceful Shutdown & Process Management

Shutdown Handler

// index.ts
import { serve } from '@hono/node-server';
import { createApp } from './app.js';
import { logger } from './utils/logger.js';
import { env } from './config/env.js';
import { db } from './config/database.js';

const app = createApp();
const server = serve({ fetch: app.fetch, port: env.PORT });

logger.info({ port: env.PORT }, 'Server started');

const shutdown = async (signal: string) => {
  logger.info({ signal }, 'Shutdown signal received');

  // Stop accepting new connections
  server.close(() => { logger.info('HTTP server closed'); });

  // Drain existing work (give 10s)
  const timeout = setTimeout(() => {
    logger.error('Forced shutdown after timeout');
    process.exit(1);
  }, 10_000);

  try {
    // Close DB pool, Redis, queues, etc.
    await db.$client.end();
    logger.info('Database pool closed');
    clearTimeout(timeout);
    process.exit(0);
  } catch (err) {
    logger.error({ err }, 'Error during shutdown');
    process.exit(1);
  }
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

// Catch unhandled errors
process.on('unhandledRejection', (err) => {
  logger.fatal({ err }, 'Unhandled rejection');
  process.exit(1);
});

Phase 11: Production Deployment

Multi-Stage Dockerfile

# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY tsconfig.json ./
COPY src/ src/
COPY drizzle/ drizzle/
RUN npm run build
RUN npm ci --omit=dev --ignore-scripts

# Production stage
FROM node:22-alpine
RUN apk add --no-cache tini dumb-init
WORKDIR /app
COPY --from=builder /app/dist dist/
COPY --from=builder /app/drizzle drizzle/
COPY --from=builder /app/node_modules node_modules/
COPY --from=builder /app/package.json .

ENV NODE_ENV=production
USER node
EXPOSE 3000
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/index.js"]

GitHub Actions CI

name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_DB: test, POSTGRES_PASSWORD: test }
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --coverage
        env: { DATABASE_URL: 'postgresql://postgres:test@localhost:5432/test' }
      - run: npm run build

Production Checklist

P0 — Mandatory:

  • [ ] NODE_ENV=production
  • [ ] Strict TypeScript (tsc --noEmit passes)
  • [ ] All env vars validated at startup
  • [ ] Health + readiness endpoints
  • [ ] Graceful shutdown handles SIGTERM
  • [ ] Structured JSON logging (no console.log)
  • [ ] Error handler doesn't leak stack traces
  • [ ] Rate limiting enabled
  • [ ] CORS configured (not wildcard)
  • [ ] Security headers set

P1 — Recommended:

  • [ ] Request ID tracing through all logs
  • [ ] Database connection pooling configured
  • [ ] Dependency audit clean (npm audit)
  • [ ] Docker image < 200MB
  • [ ] Response compression (gzip/brotli)
  • [ ] API versioning strategy decided
  • [ ] Monitoring/alerting configured

Phase 12: Performance Optimization

Priority Stack

| # | Technique | Impact | Effort | |---|-----------|--------|--------| | 1 | Connection pooling (DB + Redis) | High | Low | | 2 | Response caching (Redis/in-memory) | High | Medium | | 3 | Query optimization (indexes, N+1) | High | Medium | | 4 | JSON serialization (fast-json-stringify) | Medium | Low | | 5 | Compression middleware | Medium | Low | | 6 | Streaming responses for large payloads | Medium | Medium | | 7 | Worker threads for CPU-bound work | High | High | | 8 | HTTP/2 + keep-alive | Low | Low |

Common Optimizations

// N+1 prevention — batch with dataloader or SQL JOIN
// ❌ Bad: for loop with individual queries
for (const order of orders) {
  order.user = await db.query.users.findFirst({ where: eq(users.id, order.userId) });
}

// ✅ Good: single query with join
const ordersWithUsers = await db
  .select()
  .from(orders)
  .leftJoin(users, eq(orders.userId, users.id))
  .where(inArray(orders.id, orderIds));

// In-memory caching for hot data
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({ max: 1000, ttl: 60_000 });

async function getCachedUser(id: string) {
  const cached = cache.get(id);
  if (cached) return cached;
  const user = await userRepo.findById(id);
  cache.set(id, user);
  return user;
}

Phase 13: Background Jobs & Queues

BullMQ Pattern

// jobs/email.job.ts
import { Queue, Worker } from 'bullmq';
import { env } from '../config/env.js';
import { logger } from '../utils/logger.js';

const connection = { url: env.REDIS_URL };

export const emailQueue = new Queue('email', { connection });

export const emailWorker = new Worker('email', async (job) => {
  const { to, subject, body } = job.data;
  logger.info({ jobId: job.id, to }, 'Sending email');
  // Send via provider
  await sendEmail({ to, subject, body });
}, {
  connection,
  concurrency: 5,
  limiter: { max: 10, duration: 1000 }, // 10 per second
});

emailWorker.on('failed', (job, err) => {
  logger.error({ jobId: job?.id, err }, 'Email job failed');
});

Job Rules

  1. Idempotent jobs — same job ID running twice produces same result
  2. Retry with backoff — exponential backoff for transient failures
  3. Dead letter queue — failed jobs after max retries go to DLQ for inspection
  4. Job timeout — set removeOnComplete and removeOnFail TTLs
  5. Monitor queue health — queue length, processing time, failure rate

Phase 14: Advanced Patterns

Dependency Injection (Manual)

// app.ts — compose dependencies explicitly
export function createApp(deps?: { db?: Database; redis?: Redis }) {
  const database = deps?.db ?? defaultDb;
  const app = new Hono();

  // Compose service graph
  const userRepo = new UserRepository(database);
  const userService = new UserService(userRepo);

  // Mount routes with injected services
  app.route('/users', createUserRoutes(userService));

  return app;
}

Rate Limiting

// middleware/rate-limit.ts (using hono-rate-limiter)
import { rateLimiter } from 'hono-rate-limiter';

export const apiRateLimit = rateLimiter({
  windowMs: 60_000,
  limit: 100,
  keyGenerator: (c) => c.get('jwtPayload')?.sub || c.req.header('x-forwarded-for') || 'anonymous',
  message: { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
});

WebSocket Pattern (Hono + @hono/node-ws)

import { createNodeWebSocket } from '@hono/node-ws';

const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });

app.get('/ws', upgradeWebSocket((c) => ({
  onOpen(evt, ws) { logger.info('WebSocket connected'); },
  onMessage(evt, ws) {
    const data = JSON.parse(evt.data.toString());
    // Handle message
    ws.send(JSON.stringify({ type: 'ack', id: data.id }));
  },
  onClose() { logger.info('WebSocket disconnected'); },
})));

2025+ Recommended Stack

| Layer | Recommended | Alternative | |-------|-------------|-------------| | Runtime | Node.js 22 LTS | Bun | | Framework | Hono | Fastify | | Language | TypeScript (strict) | — | | Validation | Zod | Valibot | | ORM | Drizzle | Prisma | | Database | PostgreSQL | SQLite (dev) | | Cache | Redis / Upstash | LRU in-memory | | Auth | JWT + refresh | Lucia, Better Auth | | Queue | BullMQ | pg-boss | | Testing | Vitest | — | | Logging | Pino | — | | Linting | Biome | ESLint + Prettier | | CI/CD | GitHub Actions | — | | Deploy | Docker + Railway/Fly | Vercel (serverless) |


10 Commandments

  1. Validate everything at the boundary — Zod on every route
  2. Fail fast at startup — env vars, DB connection, migrations
  3. Structured logging everywhere — JSON, request IDs, no console.log
  4. Custom errors with codes — never throw strings
  5. Cursor pagination — never OFFSET
  6. Graceful shutdown — drain connections on SIGTERM
  7. Test error paths — 4xx/5xx matter more than happy paths
  8. Type everything — no any, branded IDs, Zod-derived types
  9. Security by default — rate limit, helmet, CORS, audit deps
  10. Keep it simple — 50 lines/function, 300 lines/file, flat is better than nested

10 Common Mistakes

| # | Mistake | Fix | |---|---------|-----| | 1 | console.log in production | Use Pino with JSON output | | 2 | any type everywhere | unknown + type narrowing | | 3 | No input validation | Zod middleware on every route | | 4 | OFFSET pagination | Cursor-based with keyset | | 5 | No graceful shutdown | SIGTERM handler with drain timeout | | 6 | Hardcoded config | Zod-validated env at startup | | 7 | Fat controllers | Thin routes → services → repositories | | 8 | No error hierarchy | Custom AppError with codes | | 9 | Testing only happy paths | Test 400/401/404/409/500 explicitly | | 10 | No request tracing | Request ID middleware + propagation |


Quality Scoring (0-100)

| Dimension | Weight | What to Assess | |-----------|--------|----------------| | Type safety | 20% | Strict TS, no any, branded types | | Error handling | 15% | Custom errors, global handler, no leaks | | Testing | 15% | Coverage >80%, integration tests, error paths | | Security | 15% | Auth, validation, rate limiting, headers | | Observability | 10% | Structured logging, health checks, metrics | | Performance | 10% | Connection pooling, caching, N+1 prevention | | Code structure | 10% | Layer separation, file size limits, DI | | Deployment | 5% | Docker, CI/CD, graceful shutdown |


Edge Cases

Startup/MVP

  • Start with Hono + SQLite (Drizzle) + Vitest — ship in hours
  • Add Postgres when you need concurrent writes
  • Skip Redis until you need caching or queues

Monorepo (Turborepo/Nx)

  • Shared packages: @org/types, @org/db, @org/validation
  • Each service is its own package with independent build/test
  • Use workspace protocol: "@org/types": "workspace:*"

Serverless (Vercel/Cloudflare Workers)

  • Hono runs everywhere — same code, different adapters
  • Use serverless DB drivers (Neon, PlanetScale, Turso)
  • No graceful shutdown needed — stateless by design

Legacy Express Migration

  • Add TypeScript incrementally (.ts files alongside .js)
  • Wrap Express routes with Zod validation middleware
  • Replace console.log with Pino one file at a time
  • Migrate to Hono/Fastify route-by-route using adapter

Natural Language Commands

  • "Set up a new TypeScript API project" → Phase 1-3 (structure + config + env)
  • "Add authentication to my API" → Phase 7 (JWT + RBAC)
  • "Review my error handling" → Phase 4 (error hierarchy + handler)
  • "Add database with Drizzle" → Phase 6 (schema + repository + migrations)
  • "Set up testing" → Phase 9 (Vitest + unit + integration patterns)
  • "Prepare for production deployment" → Phase 11 (Docker + CI + checklist)
  • "Optimize API performance" → Phase 12 (priority stack + patterns)
  • "Add background jobs" → Phase 13 (BullMQ + patterns)
  • "Audit my API security" → Phase 7 security checklist
  • "Set up logging and monitoring" → Phase 8 (Pino + request ID + health)
  • "Review my project structure" → Phase 1 (structure + rules)
  • "Full health check" → Quick Health Check + Quality Scoring

⚡ Built by AfrexAI — AI-powered business automation.

API & Reliability

Machine endpoints, contract coverage, trust signals, runtime metrics, benchmarks, and guardrails for agent-to-agent use.

MissingCLAWHUB

Machine interfaces

Contract & API

Contract coverage

Status

missing

Auth

None

Streaming

No

Data region

Unspecified

Protocol support

OpenClaw: self-declared

Requires: none

Forbidden: none

Guardrails

Operational confidence: low

No positive guardrails captured.
Invocation examples
curl -s "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/snapshot"
curl -s "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/contract"
curl -s "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/trust"

Operational fit

Reliability & Benchmarks

Trust signals

Handshake

UNKNOWN

Confidence

unknown

Attempts 30d

unknown

Fallback rate

unknown

Runtime metrics

Observed P50

unknown

Observed P95

unknown

Rate limit

unknown

Estimated cost

unknown

Do not use if

Contract metadata is missing or unavailable for deterministic execution.
No benchmark suites or observed failure patterns are available.

Machine Appendix

Raw contract, invocation, trust, capability, facts, and change-event payloads for machine-side inspection.

MissingCLAWHUB

Contract JSON

{
  "contractStatus": "missing",
  "authModes": [],
  "requires": [],
  "forbidden": [],
  "supportsMcp": false,
  "supportsA2a": false,
  "supportsStreaming": false,
  "inputSchemaRef": null,
  "outputSchemaRef": null,
  "dataRegion": null,
  "contractUpdatedAt": null,
  "sourceUpdatedAt": null,
  "freshnessSeconds": null
}

Invocation Guide

{
  "preferredApi": {
    "snapshotUrl": "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/snapshot",
    "contractUrl": "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/contract",
    "trustUrl": "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/trust"
  },
  "curlExamples": [
    "curl -s \"https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/snapshot\"",
    "curl -s \"https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/contract\"",
    "curl -s \"https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/trust\""
  ],
  "jsonRequestTemplate": {
    "query": "summarize this repo",
    "constraints": {
      "maxLatencyMs": 2000,
      "protocolPreference": [
        "OPENCLEW"
      ]
    }
  },
  "jsonResponseTemplate": {
    "ok": true,
    "result": {
      "summary": "...",
      "confidence": 0.9
    },
    "meta": {
      "source": "CLAWHUB",
      "generatedAt": "2026-04-17T02:38:40.202Z"
    }
  },
  "retryPolicy": {
    "maxAttempts": 3,
    "backoffMs": [
      500,
      1500,
      3500
    ],
    "retryableConditions": [
      "HTTP_429",
      "HTTP_503",
      "NETWORK_TIMEOUT"
    ]
  }
}

Trust JSON

{
  "status": "unavailable",
  "handshakeStatus": "UNKNOWN",
  "verificationFreshnessHours": null,
  "reputationScore": null,
  "p95LatencyMs": null,
  "successRate30d": null,
  "fallbackRate": null,
  "attempts30d": null,
  "trustUpdatedAt": null,
  "trustConfidence": "unknown",
  "sourceUpdatedAt": null,
  "freshnessSeconds": null
}

Capability Matrix

{
  "rows": [
    {
      "key": "OPENCLEW",
      "type": "protocol",
      "support": "unknown",
      "confidenceSource": "profile",
      "notes": "Listed on profile"
    },
    {
      "key": "both",
      "type": "capability",
      "support": "supported",
      "confidenceSource": "profile",
      "notes": "Declared in agent profile metadata"
    }
  ],
  "flattenedTokens": "protocol:OPENCLEW|unknown|profile capability:both|supported|profile"
}

Facts JSON

[
  {
    "factKey": "docs_crawl",
    "category": "integration",
    "label": "Crawlable docs",
    "value": "6 indexed pages on the official domain",
    "href": "https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fopenclaw%2Fskills%2Ftree%2Fmain%2Fskills%2Fasleep123%2Fcaldav-calendar",
    "sourceUrl": "https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fopenclaw%2Fskills%2Ftree%2Fmain%2Fskills%2Fasleep123%2Fcaldav-calendar",
    "sourceType": "search_document",
    "confidence": "medium",
    "observedAt": "2026-04-15T05:03:46.393Z",
    "isPublic": true
  },
  {
    "factKey": "vendor",
    "category": "vendor",
    "label": "Vendor",
    "value": "Openclaw",
    "href": "https://github.com/openclaw/skills/tree/main/skills/1kalin/afrexai-nodejs-production",
    "sourceUrl": "https://github.com/openclaw/skills/tree/main/skills/1kalin/afrexai-nodejs-production",
    "sourceType": "profile",
    "confidence": "medium",
    "observedAt": "2026-04-15T00:45:39.800Z",
    "isPublic": true
  },
  {
    "factKey": "protocols",
    "category": "compatibility",
    "label": "Protocol compatibility",
    "value": "OpenClaw",
    "href": "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/contract",
    "sourceUrl": "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/contract",
    "sourceType": "contract",
    "confidence": "medium",
    "observedAt": "2026-04-15T00:45:39.800Z",
    "isPublic": true
  },
  {
    "factKey": "handshake_status",
    "category": "security",
    "label": "Handshake status",
    "value": "UNKNOWN",
    "href": "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/trust",
    "sourceUrl": "https://xpersona.co/api/v1/agents/clawhub-skills-1kalin-afrexai-nodejs-production/trust",
    "sourceType": "trust",
    "confidence": "medium",
    "observedAt": null,
    "isPublic": true
  }
]

Change Events JSON

[
  {
    "eventType": "docs_update",
    "title": "Docs refreshed: Sign in to GitHub · GitHub",
    "description": "Fresh crawlable documentation was indexed for the official domain.",
    "href": "https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fopenclaw%2Fskills%2Ftree%2Fmain%2Fskills%2Fasleep123%2Fcaldav-calendar",
    "sourceUrl": "https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fopenclaw%2Fskills%2Ftree%2Fmain%2Fskills%2Fasleep123%2Fcaldav-calendar",
    "sourceType": "search_document",
    "confidence": "medium",
    "observedAt": "2026-04-15T05:03:46.393Z",
    "isPublic": true
  }
]

Sponsored

Ads related to afrexai-nodejs-production and adjacent AI workflows.