AgentZero API Map
Comprehensive reference for all API endpoints, server actions, and database operations.
Server Actions
All server actions are defined in /lib/actions/ and use React 19's native Server Action serialization (no custom encoding layer). Authentication is enforced at the start of each function via auth() from Auth.js v5.
Agent Management (lib/actions/agent-crud-actions.ts)
getOrgAgents()
- ›Purpose: Fetch all agents for the authenticated user's organisation
- ›Returns:
Agent[] - ›Type:
Agent = { id, name, instructions, organisation_id, created_at } - ›RLS: Scoped to session's
orgId - ›Example:
$ snippet
const agents = await getOrgAgents();
createAgent(name?: string, instructions?: string)
- ›Purpose: Create a new agent for the current organisation
- ›Params: Optional agent name and system instructions
- ›Returns:
{ success: true; id: string } | { success: false; error: string } - ›Side Effects: Auto-names unnamed agents as "Agent N"
- ›RLS: Writes scoped to authenticated user's
orgId - ›Example:
$ snippet
const result = await createAgent("My Agent", "You are a research agent..."); if (result.success) console.log(result.id);
updateAgent(agentId: string, fields: { name?: string; instructions?: string })
- ›Purpose: Update agent name and/or instructions
- ›Returns:
{ success: true } | { success: false; error: string } - ›Validation: Verifies agent belongs to session's organisation
- ›Example:
$ snippet
const result = await updateAgent(agentId, { instructions: "New system prompt" });
deleteAgents(agentIds: string[])
- ›Purpose: Delete one or more agents
- ›Returns:
{ success: true; deletedCount: number } | { success: false; error: string } - ›Side Effects: Cascades to all linked conversations and documents
- ›RLS: Only agents in session's organisation can be deleted
- ›Example:
$ snippet
const result = await deleteAgents(["uuid-1", "uuid-2"]);
duplicateAgent(agentId: string)
- ›Purpose: Clone an agent including its instructions
- ›Returns:
{ success: true; id: string } | { success: false; error: string } - ›Side Effects: Creates new agent with name suffix "(copy)"
- ›Example:
$ snippet
const clone = await duplicateAgent(originalAgentId);
Memory & Knowledge Base (lib/actions/agent-crud-actions.ts)
saveMemoSummary(content: string, agentId: string, title?: string, tags?: string[])
- ›Purpose: Save a message or insight to the agent's long-term memory
- ›Returns:
{ success: true; id: string } | { success: false; error: string } - ›Side Effects: Generates vector embedding; logs to audit with timestamps
- ›Embedding Model: OpenAI text-embedding-3-small (1536 dimensions)
- ›RLS: Stored with
user_id+organisation_idscoping - ›Example:
$ snippet
const memo = await saveMemoSummary( "Discussed quarterly roadmap with CEO", agentId, "Q1 Planning", ["business", "planning"] );
searchMemos(query: string, limit?: number, minSimilarity?: number)
- ›Purpose: Semantic search across memo summaries
- ›Params:
query(search text),limit(default 5),minSimilarity(default 0.5, range 0-1) - ›Returns:
{ success: true; results: MemoSearchResult[] } | { success: false; error: string } - ›Result Type:
MemoSearchResult = { id, agent_id, title, content, tags, similarity } - ›RLS: Results filtered to authenticated user's organisation
- ›Example:
$ snippet
const search = await searchMemos("product strategy", 10, 0.4);
Agent Streaming & Execution (lib/actions/agent-actions.ts)
streamAgentAction(prompt: string, agentId?: string, options?: {...})
- ›Purpose: Stream an agent response (ReAct loop) with real-time tool execution
- ›Params:
- ›
prompt: User query (1–10,000 chars) - ›
agentId: Optional scoped agent (validates org membership) - ›
options.modelId: Model ID override - ›
options.enabledTools: Array of tool IDs to enable (default: all) - ›
options.attachments: Content with image attachments
- ›
- ›Returns:
AsyncIterable<StreamEvent> - ›Stream Event Types:
$ snippet
type StreamEvent = | { type: "text-delta"; delta: string } | { type: "tool-call"; toolName: string; toolCallId: string; input: unknown } | { type: "tool-result"; toolName: string; toolCallId: string; output: unknown } | { type: "done" } | { type: "error"; message: string }; - ›Side Effects:
- ›Pre-flight RAG context injection (cached 60s per agent/org)
- ›Optimistic credit deduction (rolled back on error)
- ›Agent instructions override with custom system prompt
- ›Tools Available:
webSearchTool(real-time web),knowledgeSearchTool(semantic search, invisible RAG),emailAutomateTool(Resend),dbReadTool+dbWriteTool(Supabase) - ›Max Steps: 10 ReAct loop iterations before stop (via
stopWhen: stepCountIs(10)) - ›Credit Deduction: 1 credit per run (configurable)
- ›Example:
$ snippet
const stream = await streamAgentAction("What are our key metrics?", agentId); for await (const event of stream) { if (event.type === "text-delta") console.log(event.delta); }
Authentication (lib/actions/auth-actions.ts)
loginAction(prevState: null, formData: FormData)
- ›Purpose: Server Action for form-based login
- ›Input Schema:
$ snippet
{ email: string (email format), password: string (min 8 chars) } - ›Returns:
{ success: true } | { success: false; error: string } - ›Side Effects:
- ›Pre-flight email existence check (caches in React request lifecycle)
- ›Delegates to
signIn('credentials', ...)→authorize()callback - ›Argon2 password verification (no plaintext storage)
- ›Redirects to
/dashboardon success
- ›Security: Never confirms whether an email exists (enumeration prevention)
- ›Usage: Bound to login form via React 19's
useActionState(loginAction, null) - ›Example:
$ snippet
const [state, formAction] = useActionState(loginAction, null); return <form action={formAction}>...</form>;
registerAction(prevState: null, formData: FormData)
- ›Purpose: Sign-up with automatic organisation + user creation
- ›Input Schema:
$ snippet
{ name: string (min 2 chars), orgName: string (min 2 chars), email: string (email format, unique), password: string (min 8 chars) } - ›Returns:
{ success: true } | { success: false; error: string } - ›Side Effects:
- ›Creates
organisationsrow with slug (unique constraint) - ›If slug collision: retries with 4-char random suffix
- ›Creates
usersrow with argon2 password hash - ›Orphan cleanup: deletes organisation if user insert fails
- ›Auto-signs in and redirects to
/dashboard
- ›Creates
- ›Idempotency: Email uniqueness enforced; slug collision auto-recovered
- ›Example:
$ snippet
const [state, formAction] = useActionState(registerAction, null);
logoutAction()
- ›Purpose: Sign out the user
- ›Side Effects: Clears session and redirects to
/ - ›Example:
$ snippet
await logoutAction();
Conversations & Chat (lib/actions/conversation-actions.ts)
createConversation(agentId: string, title?: string)
- ›Purpose: Create a new conversation thread
- ›Returns:
{ ok: true; conversationId: string } | { ok: false; message: string } - ›Validation: Verifies agent belongs to session's organisation
- ›RLS: Conversation scoped to
organisation_id - ›Example:
$ snippet
const conv = await createConversation(agentId, "Research Session 1");
saveMessage(conversationId: string, role: "user" | "assistant", content: string)
- ›Purpose: Append a message to a conversation
- ›Returns:
{ ok: true } | { ok: false; message: string } - ›Validation: Conversation must belong to session's org
- ›Side Effects: Immutable append; updates conversation's
updated_at - ›Example:
$ snippet
await saveMessage(conversationId, "user", "What is your recommendation?");
loadConversation(conversationId: string)
- ›Purpose: Fetch all messages in a conversation
- ›Returns:
{ ok: true; messages: Array<{ id, role, content }> } | { ok: false; message: string } - ›Ordering: Chronological (oldest first)
- ›Example:
$ snippet
const { ok, messages } = await loadConversation(conversationId);
listConversations(agentId: string)
- ›Purpose: Fetch all conversations for an agent with stats
- ›Returns:
{ ok: true; conversations: ConversationSummary[] } | { ok: false; message: string } - ›Summary Type:
{ id, title, created_at, updated_at, message_count, last_message, last_message_role, last_message_at } - ›Ordering: Most recent first
- ›Efficiency: Batches message count + last message queries
- ›Example:
$ snippet
const { ok, conversations } = await listConversations(agentId);
updateConversationTitle(conversationId: string, title: string)
- ›Purpose: Rename a conversation
- ›Returns:
{ ok: true } | { ok: false; message: string } - ›Example:
$ snippet
await updateConversationTitle(conversationId, "Q1 Planning Notes");
Billing & Credits (lib/actions/billing-actions.ts)
createProCheckout(input?: { redirectPath?: string })
- ›Purpose: Generate a Lemon Squeezy checkout URL for Pro tier
- ›Params: Optional site-relative redirect path (e.g.,
/dashboard?tier=pro) - ›Returns:
{ ok: true; url: string } | { ok: false; error: string } - ›Side Effects: LS API call with
user_idin custom_data (webhook idempotency) - ›Security: Validates
redirectPathto prevent open redirects - ›Example:
$ snippet
const checkout = await createProCheckout({ redirectPath: "/dashboard" }); if (checkout.ok) window.location.href = checkout.url;
createFoundingCheckout(input?: { redirectPath?: string })
- ›Purpose: Generate a checkout for the Founding tier (one-time purchase)
- ›Returns:
{ ok: true; url: string } | { ok: false; error: string } - ›Example:
$ snippet
const checkout = await createFoundingCheckout();
Credits & Pre-Flight (lib/actions/credit-actions.ts)
checkUserCredits(userId: string)
- ›Purpose: Verify user has non-zero credits before executing agent
- ›Returns:
{ ok: true } | { ok: false; status: 402; message: string } - ›Side Effects: None; read-only
- ›Idempotency: Called once before
streamAgentActionopens the stream - ›Example:
$ snippet
const check = await checkUserCredits(session.user.id); if (!check.ok) return; // Insufficient credits
deductCredits(userId: string, amount?: number)
- ›Purpose: Optimistically lock credits at stream start
- ›Params:
amountdefaults to 1 - ›Returns:
{ ok: true; deducted: number } | { ok: false; status: 402; message: string } - ›Side Effects: Atomic RPC call to
deduct_user_credits(p_user_id, p_amount) - ›Rollback: Return value passed to
rollbackCredits()on error - ›Example:
$ snippet
const deduct = await deductCredits(userId, 1); if (deduct.ok) { // Stream agent, keep deducted amount }
rollbackCredits(userId: string, amount: number)
- ›Purpose: Refund credits on stream failure or tool error
- ›Returns:
{ ok: true } | { ok: false; message: string } - ›Side Effects: Calls
refund_user_credits(p_user_id, p_amount)RPC - ›Error Handling: Never re-throws; logs for manual reconciliation if RPC fails
- ›Example:
$ snippet
try { // stream agent } catch (err) { if (deduction?.ok) await rollbackCredits(userId, deduction.deducted); }
Document Upload & Processing (lib/actions/document-actions.ts)
uploadDocument(formData: FormData)
- ›Purpose: Upload and chunk a PDF, TXT, or Markdown file
- ›Input: FormData with
file(File) and optionalagentId(string) - ›Returns:
{ success: true; documentId: string; chunkCount: number } | { success: false; error: string } - ›Validation:
- ›File types: PDF, TXT, MD (max 10 MB)
- ›Minimum 1 character of extractable text
- ›Pipeline:
- ›Extract text (unpdf for PDFs, native for text)
- ›Chunk by 500 chars (50 char overlap)
- ›Generate embeddings (OpenAI text-embedding-3-small)
- ›Insert document row + chunks atomically
- ›Rollback on chunk store failure (prevents orphans)
- ›Side Effects: Creates document + document_chunks rows; vectors stored in pgvector
- ›Chunking Config:
CHUNK_SIZE=500,CHUNK_OVERLAP=50(hardcoded in source) - ›Example:
$ snippet
const formData = new FormData(); formData.append("file", file); formData.append("agentId", agentId); const result = await uploadDocument(formData);
listOrgDocuments()
- ›Purpose: Fetch all documents uploaded by the organisation
- ›Returns:
{ ok: true; documents: Array<{ id, file_name, file_type }> } | { ok: false; error: string } - ›Ordering: Most recent first
- ›Example:
$ snippet
const { ok, documents } = await listOrgDocuments();
getDocumentContent(documentId: string)
- ›Purpose: Fetch the full extracted text of a document
- ›Returns:
{ ok: true; content: string } | { ok: false; error: string } - ›Use Case: Preview in Knowledge UI, re-chunking, export
- ›Example:
$ snippet
const { ok, content } = await getDocumentContent(documentId);
HTTP API Routes
All routes use Next.js App Router (/app/api/) with native request/response handling.
Authentication (/app/api/auth/[...nextauth]/route.ts)
Delegated: Routes to handlers from @/auth (Auth.js v5 configuration).
- ›
GET /api/auth/signin - ›
POST /api/auth/signin(credentials callback) - ›
GET /api/auth/callback/credentials - ›
GET /api/auth/session - ›
POST /api/auth/signout - ›
GET /api/auth/providers
Lemon Squeezy Webhooks (/app/api/webhooks/lemonsqueezy/route.ts)
POST /api/webhooks/lemonsqueezy
- ›Purpose: Receive and process Lemon Squeezy billing events
- ›Authentication: HMAC-SHA256 signature verification (header:
x-signature) - ›Security:
- ›Validates raw body signature before JSON parsing
- ›Returns 401 on invalid signature
- ›Timing-safe comparison to prevent timing attacks
- ›Idempotency:
- ›Primary: SHA-256 hash of raw body →
webhook_events.body_hash(UNIQUE) - ›Secondary:
ls_order_idorls_subscription_iddeduplication in RPC layer - ›Duplicate receipt returns
{ ok: true, duplicate: true }
- ›Primary: SHA-256 hash of raw body →
- ›Supported Event Families:
- ›Subscriptions:
subscription_created,subscription_updated,subscription_expired,subscription_paused,subscription_resumed,subscription_cancelled - ›Invoices:
subscription_payment_success,subscription_payment_failed,subscription_payment_recovered,subscription_payment_refunded - ›Orders:
order_created,order_refunded
- ›Subscriptions:
- ›Dispatch Logic:
$ snippet
subscription_* → upsertSubscription() subscription_payment_success/recovered → grantProCreditsForInvoice() subscription_payment_refunded → revokeProCreditsForInvoice() order_created → handleFoundingOrder() order_refunded → handleFoundingRefund() - ›Error Handling:
- ›Returns 500 on handler failure → LS retries with exponential backoff
- ›Returns 400 on malformed JSON or missing event_name
- ›Logs all failures for ops dashboard review
- ›Response:
$ snippet
{ "ok": true } // or { "ok": true, "duplicate": true } // or { "error": "..." } with appropriate status code
Waitlist (/app/api/waitlist/route.ts)
POST /api/waitlist
- ›Purpose: Sign up an email for the pre-launch waitlist
- ›Request Body:
$ snippet
{ "email": "user@example.com" } - ›Validation:
- ›Email format (Zod email validator)
- ›Unique constraint on
waitlist.email
- ›Response:
$ snippet
{ "success": true } // 201 Created { "error": "You are already on the waitlist." } // 409 Conflict { "error": "Failed to join waitlist. Please try again." } // 500 - ›Side Effects: Fire-and-forget email notification via Resend to
raynhardt34@gmail.com - ›Example:
$ snippet
curl -X POST http://localhost:3000/api/waitlist \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com"}'
Database Layer
All database access uses the Supabase admin client (lib/supabase/admin.ts) which bypasses Row Level Security. RLS is enforced at the application layer via organisation scoping in Server Actions.
Cached Query Functions (lib/supabase/queries.ts)
getUserByEmail(email: string)
- ›Purpose: Fetch a user by email for login pre-flight check
- ›Returns:
UserRow | null - ›Type:
UserRow = { id, email, name, organisation_id, password_hash } - ›Caching: React's
cache()deduplicates identical calls within a single request - ›Example: Called twice during login (pre-flight +
authorize()callback); runs exactly one query - ›Location:
/lib/supabase/queries.ts
Database Client (lib/supabase/admin.ts)
- ›URL:
NEXT_PUBLIC_SUPABASE_URL(public, injected at build time) - ›Key:
SUPABASE_SECRET_KEY(secret, service role, server-only) - ›Auth: Disabled (
autoRefreshToken: false,persistSession: false) — server-side only - ›Usage: All Server Actions and webhooks use this client
- ›Important: Never export to client components (taint API in next.config.ts prevents it)
Key RPC Functions (Supabase)
Credit System
deduct_user_credits(p_user_id UUID, p_amount INT)
- ›Purpose: Atomically decrement
credits_remaining, incrementcredits_used - ›Error: Raises exception if
credits_remaining < p_amountor row not found - ›Called By:
deductCredits()Server Action before streaming agent
refund_user_credits(p_user_id UUID, p_amount INT)
- ›Purpose: Reverse a deduction on error
- ›Idempotency: Safe to call multiple times (same amount)
- ›Called By:
rollbackCredits()Server Action in error handler
grant_user_credits(p_user_id UUID, p_amount INT)
- ›Purpose: Add credits (subscription or founding purchase)
- ›Validation:
p_amount > 0 - ›Called By: Billing webhook handlers (
grantProCreditsForInvoice,handleFoundingOrder)
grant_founding_credits(p_user_id UUID, p_ls_order_id TEXT, p_amount INT)
- ›Purpose: Atomically insert founding grant + add credits
- ›Idempotency:
ON CONFLICT (ls_order_id) DO NOTHINGprevents duplicate grants - ›Returns:
'granted' | 'already_exists' - ›Called By:
handleFoundingOrder()webhook handler
revoke_founding_credits(p_user_id UUID, p_ls_order_id TEXT, p_amount INT)
- ›Purpose: Reverse a founding grant on refund
- ›Behaviour:
- ›If
credits_remaining >= amount: deduct and markrevoked_at - ›If
credits_remaining < amount: flagneeds_manual_review(user may have already spent credits)
- ›If
- ›Returns:
'revoked' | 'needs_review' | 'not_found' | 'already_revoked' - ›Called By:
handleFoundingRefund()webhook handler
revoke_subscription_invoice_credits(p_user_id UUID, p_ls_subscription_id TEXT, p_amount INT)
- ›Purpose: Reverse pro subscription invoice credits on refund
- ›Same Logic: As
revoke_founding_credits()but flags subscriptions row instead - ›Called By:
revokeProCreditsForInvoice()webhook handler
match_chunks(query_embedding VECTOR, match_threshold FLOAT, match_count INT, filter_org_id UUID)
- ›Purpose: Semantic search across org-level document chunks
- ›Algorithm: pgvector cosine distance (
<=>) - ›Returns:
{ content TEXT, similarity FLOAT }[] - ›Called By:
semanticSearch()in RAG context fetch
match_agent_chunks(query_embedding VECTOR, match_threshold FLOAT, match_count INT, filter_agent_id UUID)
- ›Purpose: Semantic search scoped to a specific agent's documents
- ›Algorithm: Same as
match_chunks()but JOINs documents and filters byagent_id - ›Returns:
{ content TEXT, similarity FLOAT }[] - ›Called By:
semanticSearchForAgent()in RAG context fetch
match_memo_summaries(query_embedding VECTOR, match_threshold FLOAT, match_count INT, filter_org_id UUID)
- ›Purpose: Semantic search across organisation's memo summaries
- ›Returns: Full memo rows with similarity scores
- ›Called By:
searchMemos()Server Action
Key Database Tables
Core Multi-Tenancy
| Table | Purpose | Key Columns |
|-------|---------|------------|
| organisations | Tenant root | id (PK), name, slug (UNIQUE), created_at |
| users | Users tied to orgs | id (PK), email (UNIQUE), organisation_id (FK), password_hash, created_at |
| agents | Org-scoped AI agents | id (PK), name, instructions, organisation_id (FK), created_at |
Conversations
| Table | Purpose | Key Columns |
|-------|---------|------------|
| conversations | Chat threads per agent | id (PK), agent_id (FK), organisation_id (FK), title, created_at, updated_at |
| conversation_messages | Immutable message history | id (PK), conversation_id (FK), role ('user' \| 'assistant'), content, created_at |
Documents & Knowledge
| Table | Purpose | Key Columns |
|-------|---------|------------|
| documents | Uploaded PDFs/TXT/MD | id (PK), user_id (FK), org_id (FK), agent_id (FK, nullable), file_name, file_type, content (full text), created_at |
| document_chunks | Embedded text chunks | id (PK), document_id (FK), content, chunk_order, embedding (pgvector 1536), created_at |
| memo_summaries | Long-term memory | id (PK), user_id (FK), organisation_id (FK), agent_id (FK), content, title, tags (TEXT[]), embedding (pgvector 1536), created_at |
Credits & Billing
| Table | Purpose | Key Columns |
|-------|---------|------------|
| user_credits | Credit ledger | user_id (PK/FK), credits_remaining (INT), credits_used (INT) |
| subscriptions | LS subscription history | id (PK), user_id (FK), ls_subscription_id, tier ('pro' \| 'founding'), status, renews_at, ends_at, needs_manual_review, created_at, updated_at |
| founding_grants | Founding purchases | id (PK), user_id (FK), ls_order_id (UNIQUE), amount, granted_at, revoked_at, needs_manual_review, review_reason |
| webhook_events | LS webhook log (idempotency) | id (PK), body_hash (UNIQUE), event_name, raw_payload (JSONB), processed_at, error, received_at |
Environment Variables
Required
| Variable | Purpose | Example |
|----------|---------|---------|
| NEXT_PUBLIC_SUPABASE_URL | Supabase project URL | https://project.supabase.co |
| SUPABASE_SECRET_KEY | Supabase service role key | sb_secret_... |
| NEXTAUTH_SECRET | Auth.js session encryption | 32+ char random string |
| NEXT_PUBLIC_APP_URL | App's public URL (for redirects) | https://boileragent.dev |
Optional
| Variable | Purpose | Default |
|----------|---------|---------|
| RAG_MATCH_THRESHOLD | Embedding similarity threshold (0-1) | 0.1 |
| LEMONSQUEEZY_WEBHOOK_SECRET | LS webhook HMAC key | (required for webhooks) |
| RESEND_API_KEY | Resend email API key | (required for waitlist emails) |
Tool Registry
Tools are defined in /features/tools/, exported from features/tools/index.ts, and injected into the streamText ReAct loop. The mapping between registry IDs (used by the UI toggles) and exported names is defined in streamAgentAction():
$ snippetconst idToExportName: Record<string, string[]> = { web_search: ["webSearchTool"], knowledge: ["knowledgeSearchTool"], email: ["emailAutomateTool"], database: ["dbReadTool", "dbWriteTool"], };
User-visible tool metadata (label + icon, used to render the UI toggle list) lives separately in lib/ai/tool-registry.ts. Keep these three places in sync when adding a tool.
Available Tools
- ›
webSearchTool— Real-time web search via Tavily (user-controlled via UI toggle). - ›
knowledgeSearchTool— Semantic search over the org's uploaded documents. Always available to the model as invisible RAG plumbing; not user-toggleable. - ›
emailAutomateTool— Send transactional email via Resend (user-controlled). - ›
dbReadTool/dbWriteTool— Read from / insert into Supabase tables (user-controlled, single "Database" toggle).
Error Handling & Status Codes
| Code | Meaning | Action | |------|---------|--------| | 200 | Success | Continue | | 201 | Created (POST) | Success | | 400 | Malformed input | Fix request, retry | | 401 | Unauthorised (webhook signature) | Log signature issue, alert ops | | 402 | Payment required (insufficient credits) | Prompt user to purchase | | 409 | Conflict (duplicate email/slug) | Retry with alternative value | | 422 | Validation error | Fix input per Zod error | | 500 | Server error | LS webhook returns 500 to trigger retry |
Client-Server Flow Example: Stream Agent
$ snippetClient: form.onSubmit() ↓ Client: <form action={streamAgentAction}> ↓ Server: streamAgentAction(prompt, agentId) ├─ auth() → validates session, reads orgId ├─ checkUserCredits() → 402 if insufficient ├─ deductCredits() → locks credits ├─ fetchRagContext() → cache 60s, inject into system prompt ├─ streamText() → opens ReAct loop (max 10 steps) │ ├─ yield text-delta events │ ├─ yield tool-call events │ ├─ execute tools, yield tool-result events │ └─ yield done └─ catch: rollbackCredits() on error ↓ Server: Returns AsyncIterable<StreamEvent> immediately ↓ Client: for await (const event of stream) { ... } ├─ accumulates text-delta ├─ renders tool calls └─ displays final response (Optional): await saveMessage(conversationId, "assistant", finalText)
Rate Limiting & Quotas
- ›Agent runs: Limited by user's credit balance (configurable cost)
- ›Embedding generation: Sequential per document (avoid OpenAI rate limit bursts)
- ›Webhooks: Lemon Squeezy retries with exponential backoff (idempotent via body_hash)
- ›No explicit HTTP rate limit on Server Actions (Next.js runtime handles it)
Troubleshooting
"Unauthorised" on Server Action
- ›Check:
session?.user?.idexists (auth() call) - ›Check:
session?.user?.orgIdexists (organisation record linked)
RAG returns no results
- ›Check: Documents uploaded to agent or organisation
- ›Check:
RAG_MATCH_THRESHOLDenv var (default 0.1, increase for stricter matching) - ›Check: Embeddings generated successfully (logs may show warnings)
Webhook signature verification fails
- ›Verify:
LEMONSQUEEZY_WEBHOOK_SECRETis correct (from LS dashboard) - ›Verify: Raw body is read before JSON parsing (code does this correctly)
- ›Check: LS API configuration points to correct endpoint
Duplicate webhook event processed
- ›Check:
webhook_events.body_hashis unique (migration enforces this) - ›Check: Processed handler returns 2xx status (LS stops retrying)
- ›Check: Logs show
{ ok: true, duplicate: true }on replay
Credits not deducted
- ›Check:
user_creditsrow exists for user (created on registration) - ›Check:
deduct_user_creditsRPC executed successfully - ›Check: Check logs for "optimistic deduction" entries