Guidelines for AI agents working with this Next.js 15 + Elysia API codebase.
| Task | Command |
|---|---|
| Single test | bun test path/to/file.test.ts |
| All E2 API tests | bun run test:e2 |
| Legacy API tests | bun run test-api |
| SDK tests | bun run test-sdk |
| Lint | bun run lint |
| Generate OpenAPI | bun run generate:openapi |
bun run dev/bun run build- Ask before runningbunx drizzle-kit generate/push- Prompt user to run manuallynpx tsc- Never run (breaks project state)
Only use bun - Never npm, yarn, or pnpm.
app/api/
├── e2/ # Elysia API (primary)
│ ├── domains/ # Domain endpoints
│ ├── emails/ # Email endpoints
│ ├── lib/ # Auth, types, responses
│ └── [[...slugs]]/route.ts
└── v2/ # Legacy Next.js routes
lib/db/schema.ts # Drizzle schema (type source of truth)
features/ # Feature-specific logic
// Order: external packages -> local modules -> types
import { Elysia, t } from "elysia";
import { db } from "@/lib/db";
import { emailDomains } from "@/lib/db/schema";
import type { InferSelectModel } from "drizzle-orm";Always use @/ path alias - Never relative paths like ../../../.
- No
any- Biome enforcesnoExplicitAny: error - Find existing types in
lib/db/schema.ts- don't duplicate - Infer DB types:
InferSelectModel<typeof tableName> - Route params:
params: Promise<{ id: string }>, thenconst { id } = await params
- Drizzle ORM only - No raw SQL
- Use
structuredEmails- NOT deprecatedreceivedEmails/parsedEmails - Always scope queries by
userIdfor multi-tenant safety
- Files:
kebab-case.ts - React components:
PascalCase.tsx - Hooks:
useprefix + Query/Mutation suffix (useDomainsQuery.ts) - Tests: Same name +
.test.ts
- Use variant props for styling - never custom colors/sizes/border-radius
- Colors from CSS variables in
global.css - Use Suspense with fallback for async data
- Use TanStack Query (
useQuery,useMutation) for data fetching
- No comments unless explicitly requested
- No unnecessary README files
Always use status-code keyed objects, NOT t.Union():
// CORRECT - All responses properly documented
response: {
200: SuccessResponse,
400: ErrorResponse,
401: ErrorResponse,
404: ErrorResponse,
500: ErrorResponse,
}
// WRONG - won't show in OpenAPI docs
response: t.Union([SuccessResponse, ErrorResponse])import { createErrorResponse } from "./lib/responses";
set.status = 400;
return createErrorResponse(400, "Bad Request", "Validation failed");| Code | Use Case |
|---|---|
| 200 | Success (GET, PATCH, DELETE) |
| 201 | Created (POST) |
| 400 | Validation error |
| 401 | Auth required |
| 404 | Not found |
| 409 | Conflict |
| 500 | Server error |
Tests use bun:test against the dev API:
import { describe, it, expect } from "bun:test";
const API_URL = "https://dev.inbound.new/api/e2";
async function apiRequest(endpoint: string, options: RequestInit = {}) {
return fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${process.env.INBOUND_API_KEY}`,
"Content-Type": "application/json",
...options.headers,
},
});
}
describe("Domains API", () => {
it("should list domains", async () => {
const response = await apiRequest("/domains");
expect(response.status).toBe(200);
});
});All list endpoints return:
{
data: Item[],
pagination: { limit: number, offset: number, total: number, hasMore: boolean }
}- Don't duplicate types - use
lib/db/schema.ts - Don't use deprecated tables (
receivedEmails/parsedEmails) - Don't forget user scoping in DB queries
- Don't use
t.Union()for Elysia responses - Don't run drizzle-kit commands directly
Ban enforcement is already built-in - setting user.banned = true blocks access immediately:
app/api/e2/lib/auth.ts:168-220- Validates ban status on every E2 API requestlib/email-management/outbound-send-guard.ts:194-209- Blocks banned users from sending emailsapp/api/e2/helper/main.ts:92-129- Validates ban status for V2 API (legacy)
Ban fields in user table:
banned(boolean) - Primary ban flagbanReason(text) - Displayed to user on 403 responsesbanExpires(timestamp) - Optional expiry;null= permanent
Admin endpoints (require admin role):
POST /admin/users/:userId/ban- Ban user, set reason/expiryPOST /admin/users/:userId/unban- Clear ban, reset fields
Additional enforcement when banning for abuse:
- Suspend tenant - Set
sesTenants.status = "suspended"and callsuspendTenantSending()to disable AWS SES config set - Cancel scheduled emails - Update
scheduledEmailswherestatus IN ("scheduled", "processing")to"cancelled" - See
scripts/ban-user.tsfor complete ban workflow including tenant suspension and scheduled email cleanup
Tenant suspension helpers:
lib/aws-ses/aws-ses-tenants.ts:1160-suspendTenantSending(configSetName, reason)- disables AWS SES sendinglib/aws-ses/aws-ses-tenants.ts:1153-pauseTenantSending(configSetName, reason)- for temporary pauses- Both update
sesTenants.statusin DB and call AWS SESPutConfigurationSetSendingOptionsCommand
- Don't skip
validateAndRateLimit()in handlers