Backend
Server Actions, API routes, oRPC, middleware, rate limiting, background jobs, and server-side patterns.
Server Actions
The 4-Step Pattern
Every Server Action in app/actions/ follows this sequence:
"use server";
import { z } from "zod";
import { requireUser } from "@/lib/auth/server";
import { db } from "@/lib/db";
import { revalidatePath, revalidateTag } from "next/cache";
import { CACHE_TAGS } from "@/lib/cache";
const schema = z.object({
title: z.string().min(1).max(200),
});
export async function createResource(input: z.infer<typeof schema>) {
// 1. Auth check
const user = await requireUser();
// 2. Zod validation
const parsed = schema.safeParse(input);
if (!parsed.success) return { success: false, error: "Invalid input" };
// 3. Drizzle operation
const [result] = await db
.insert(table)
.values({ ...parsed.data, userId: user.id })
.returning({ id: table.id, title: table.title });
// 4. Cache invalidation
revalidatePath("/dashboard/resources");
revalidateTag(CACHE_TAGS.dashboard, "max");
return { success: true, data: result };
}requireUser() / verifyAdmin() always runs before validation. Validation runs before any Drizzle call. Mutations end with route and tag revalidation; revalidateTag() uses the "max" profile for stale-while-revalidate behavior.
Data Access Efficiency
Backend reads minimize payload size before optimizing transport:
- Drizzle reads project explicit columns. Bare
.select()is prohibited in actions, API routes, background handlers, and webhook administration. - List endpoints clamp caller-controlled limits. Cursor-based reads cap at 100 rows; offset-based reads cap at 200 rows unless a narrower domain cap exists.
- Webhook administration reads use
WEBHOOK_LOG_LIST_PROJECTIONandWEBHOOK_LOG_RETRY_PROJECTION; retry and list paths never hydrate unused webhook columns. - Background subscription/trial reminder jobs load only
email,fullName, andsubscriptionTierfromprofiles. - Independent async reads use
Promise.all()unless one result is required to construct another query.
Action Inventory
| File | Actions |
|---|---|
user.ts | Profile CRUD, avatar upload, account deletion with cascade |
billing.ts | Checkout, portal, credit deduction, subscription sync |
organization.ts | Org CRUD, invite/remove/role change (barrel export) |
projects.ts | Project CRUD with org or user ownership |
super-admin.ts | Super-admin user management, impersonation, system health (barrel export) |
support-tickets.ts | Ticket CRUD, replies, status transitions, archive/unarchive/delete lifecycle, AI canned responses |
api-keys.ts | Key generation (SHA-256), revocation, usage tracking |
dashboard.ts | SSR-safe data fetching for metrics, charts, and activity logs with mock fallback |
super-admin/legal.ts | AI legal doc generation, version control, publish/unpublish |
contact.ts | Contact form submission, super-admin status management, internal notes |
affiliate.ts | Registration, referral tracking, payouts |
waitlist.ts | Pre-launch signup |
newsletter.ts | Newsletter subscription, super-admin CRUD, source tracking |
onboarding.ts | Goal selection, profile completion tracking |
mfa.ts | TOTP enrollment, challenge, unenrollment |
passkeys.ts | WebAuthn registration + login |
impersonation.ts | Super-admin user impersonation session management |
super-admin/_shared.ts | Internal requireAdmin() wrapper forActionResult pattern |
️ Administrative Patterns
API Key Architecture
The platform provides a secure developer API key management system:
- Generation & Storage: Keys are generated securely (e.g.,
sk_live_...). A truncated preview is stored along with an SHA-256 hash of the full key for validation, ensuring zero plaintext storage of active tokens. - Revocation: Keys can be revoked immediately via the API key management UI.
- Usage Tracking: Each key tracks
lastUsedAtto help users audit inactive keys.
Identity-Aware Resolution
Administrative modules (Contacts, Waitlist, Newsletter) must implement the Profile Resolution pattern to link incoming data with registered user accounts.
- Trigger: Incoming email address matches a record in the
profilestable. - Implementation: Super-Admin views perform a relational lookup to fetch the user's
avatar_url,full_name, andusername. - UI Benefit: Displays official identity markers (avatars) instead of anonymous initials, improving staff oversight.
Standardized CSV Data Extraction
Exporting administrative data (Support Tickets, Contacts, Newsletter) is standardized via a frontend-driven streaming pattern:
- Selection: Filtered data is retrieved based on the current dashboard state (search/sort).
- Transformation: JSON records are converted to CSV strings with double-quote escaping for all fields.
- Encoding: Prepend
\uFEFF(Byte Order Mark) to ensure proper UTF-8 character handling in Excel/Numbers. - Delivery: Blob-based download using the
application/csvMIME type.
Centralized Account Deletion
The system implements a mandatory single-entry-point for permanent account destruction via performAccountDeletion(userId, email) in lib/services/account-deletion.ts.
- Fail-closed ordering: The service completes external and storage cleanup before final Auth deletion. Any critical failure returns a failed result and stops local destruction.
- Fail-closed billing safety: If any active-like stored provider subscription cannot be matched to a provider subscription ID or immediately canceled, the service returns a failed deletion result before deleting profile, storage, waitlist, or Supabase Auth records.
- Cleanup Chain:
- Billing: Cancels active subscriptions at the payment provider.
- Storage: Deletes all user-owned files under the user prefix from every configured Supabase Storage bucket.
- Logs: Purges user-scoped activity logs while writing a retained, privacy-minimized
auth_events.account_deletedaudit event. - Marketing: Removes the user's email from
waitlist_subscribersto ensure PII cleanup. - Auth: Deletes the Supabase Auth user record via the
service_rolesuper-admin client. - Profile: Cascades from the
auth.usersdeletion and is not treated as an independent best-effort cleanup.
Return Pattern
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };No action throws to client. Error messages use i18n via getTranslations().
Checkout creation deliberately returns generic translated errors such as checkoutCreateError and checkoutSessionError instead of raw provider messages. Provider SDK errors stay in server logs/Sentry; client responses remain stable and do not expose internal payment failure details.
oRPC API Layer
Type-safe RPC at /api/rpc/* via oRPC. Generates OpenAPI 3.0.3. Interactive docs via Scalar at /api-references.
lib/rpc/
├── server.ts # publicProcedure, protectedProcedure, superAdminProcedure
├── router.ts # Root router & OpenAPI contract definition
├── client.ts # Type-safe client (shared)
└── routers/
├── super-admin.ts # Super-Admin operations
├── billing.ts # Credits, usage
├── developer.ts # Developer API key management
├── health.ts # Service health
├── support.ts # AI chat, tickets
└── user.ts # User profile operations| Procedure | Auth | Use Case |
|---|---|---|
publicProcedure | No | Public, aggregate health |
protectedProcedure | Yes | User data |
superAdminProcedure | Yes + Super-Admin | Super-Admin ops |
Error handling:
throw new ORPCError("NOT_FOUND", {
message: t("Common.serverErrors.notFound"),
});Codes: NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, INTERNAL_SERVER_ERROR.
Client: Base URL from config.app.url, not hardcoded localhost. Browser calls use the same-origin /api/rpc transport and send X-CSRF-Token when the middleware-seeded CSRF cookie exists.
The public OpenAPI document intentionally exposes only platform roles that exist in profiles.role: user and super-admin. Organization roles are separate membership values and must not be documented as platform profile roles.
Edge Middleware (proxy.ts)
Runs on every request before App Router:
- Route classification — public / auth / protected / super-admin / API
- Rate limiting — Upstash Redis, per-tier from
lib/config/pricing.ts - Circuit breaker — auto IP ban after 5 violations
- CSP nonce — per-request,
strict-dynamic - CSRF — HMAC-bound double-submit cookie + user binding; authenticated page requests seed/refresh
__csrf_token; rootCSRFProviderexposescsrfFetch()to public and protected client surfaces; authenticated support chat and invite acceptance require CSRF, guest chat is allowed only when no session exists - Session refresh — Supabase token rotation
- MFA enforcement — AAL2 check, redirect if not met (fail-closed on error)
- Feature flags — disabled modules →
/dashboard - Header injection —
x-pathname,x-org-id,x-nonce,x-impersonating-* - Impersonation context — verifies signed cookie, 1-hour auto-expiry tracking, and read-time super-admin/target revalidation
- Locale negotiation — Accept-Language detection +
NEXT_LOCALEcookie persistence
Production deployments treat the edge limiter as security-critical: if Upstash
Redis credentials are missing or unavailable outside development, middleware
fails closed and internal health reports Redis as unhealthy. Client IP parsing
must go through getClientIp(); forwarding headers are trusted only on Vercel
or when TRUST_PROXY_HEADERS=true is set behind an ingress layer that strips
untrusted forwarding headers.
Route Classification
| Group | Pattern | Behavior |
|---|---|---|
| Public | /, /pricing, /blog/* | No auth, rate limited |
| Auth | /login, /signup, /forgot-* | Redirect to dashboard if logged in |
| Protected | /dashboard/*, /dashboard/billing | Redirect to login if not authed |
| Super-Admin | /super-admin/* | Require super-admin role |
| API | /api/* | Rate limiting, Same-origin only |
API Routes
CORS & Origin Security
The Vibe44 API enforces a strict Same-Origin Policy by default. There is no global CORS implementation in proxy.ts or next.config.ts.
- Internal Requests: All frontend-to-backend communication (Server Actions, oRPC, and internal
fetch) works on the same origin. Browser oRPC calls includeX-CSRF-Tokenwhen the middleware-seeded CSRF cookie exists; mutating browserfetch()calls must usecsrfFetch()unless the route is explicitly public. - External Requests: Cross-origin requests are blocked at the browser level, providing a secure "fail-closed" default against unauthorized third-party access.
- Exceptions: Webhook endpoints (
/api/webhooks/*) are exempt from CSRF but still follow same-origin rules for standard browser access.
| Route | Method | Purpose |
|---|---|---|
/api/auth/callback | GET | OAuth, magic link, and password-reset code exchange (PKCE) |
/api/health | GET | Minimal public liveness metadata |
/api/legal | GET | Public published legal docs (rate-limited, CDN-cached) |
/api/rpc/[...path] | ALL | oRPC typed API; public health is aggregate-only, dependency health is super-admin-only |
/api/webhooks/stripe | POST | Stripe webhooks |
/api/webhooks/polar | POST | Polar webhooks |
/api/webhooks/lemon-squeezy | POST | Lemon Squeezy webhooks |
/api/webhooks/dodo | POST | Dodo webhooks |
/api/webhooks/creem | POST | Creem webhooks |
/api/auth/passkey/register/* | POST | WebAuthn registration |
/api/auth/passkey/login/* | POST | WebAuthn authentication |
/api/chat | POST | AI chat (SSE streaming) |
/api/support/voice | GET/POST | Voice catalog and TTS via ElevenLabs/Murf |
/api/support/voice/stt | POST | Speech-to-text via ElevenLabs; accepts multipart audio file |
/api/support/transcribe | POST | Legacy speech-to-text route; accepts multipart file |
/api/super-admin/* | ALL | Super-Admin-only REST endpoints (Blog, Knowledge Base, Tickets, etc.) |
API routes validate every route param, query param, and JSON body with local Zod schemas before using values in Drizzle predicates. Super-Admin routes call verifyAdmin() first, then validate params, then run explicit-column queries or mutations.
API routes and Server Actions must not return parsed.error.flatten() or raw provider exceptions. User, support, developer, super-admin, checkout, upload, and voice surfaces return stable translated/domain errors; detailed validation failures stay in server logs and Sentry.
REST billing routes mirror Server Action identity semantics. Checkout, portal,
cancel, and subscription routes resolve getEffectiveUserId(user.id) before
reading or mutating billing state so super-admin impersonation targets the
effective account consistently.
Account deletion delegates to performAccountDeletion(), which fails closed when
subscription cancellation, storage cleanup, or Supabase Auth deletion fails. The
service logs the underlying failure server-side and returns stable deletion
failure copy to callers.
User-facing support chat routes treat conversationId as an untrusted reference.
Authenticated requests resolve it with getConversationForUser() and the
effective user ID before loading message history, appending user/agent
messages, broadcasting updates, changing conversation status, or creating
escalation tickets.
Affiliate Tracking And Conversion
Affiliate links must point to /api/affiliate/track?ref=<code>&redirect=<path>.
The tracking route validates relative redirects, records the sanitized landing
path, rate-limits click inflation, and sets affiliate_ref / referral_id
cookies on the redirect response. Payment providers pass the buyer user ID into
convertAffiliateReferral(referralId, amount, userId) so convertReferral()
can block self-referrals before commission crediting. Expired referrals are
marked expired, non-positive payment amounts are rejected, and payment-originated
cache invalidation includes the affiliate tag.
Affiliate click privacy is write-time. Raw IP addresses and browser user agents are hashed before referral persistence. Affiliate-facing stats return attribution and commission fields only; they must not expose raw or hashed tracking identifiers to affiliate users.
Affiliate payout profile storage lives on the affiliates row. New and
re-submitted applications must capture payout_method (wise, paypal, or
payoneer) and normalized payout_email; requestPayout() uses that stored
profile unless an explicit method/email override is provided. The minimum
payout threshold is not hardcoded in actions or UI; change
config.affiliate.minimumPayoutThresholdInCents in lib/config/app.ts.
️ AI Legal Document Generation
AI-powered legal doc generation via lib/legal-generator/index.ts + super-admin Server Actions in app/actions/super-admin/legal.ts.
Generation Flow
Super-Admin triggers generateLegalDocAction(type, locale)
↓
getAIProvider().generateStructuredOutput()
Structured output (responseMimeType: "application/json", responseSchema)
↓
temperature: 0.3 (deterministic) + LegalDocContext personalization
↓
{ title, content } → INSERT into generatedLegalDocs (version auto-incremented)Universal Structured Output Mapping
To support multiple AI providers under a single unified interface, AIProvider.generateStructuredOutput dynamically maps Zod or JSON schemas to each provider's native API:
- Gemini: Sourced natively via
responseSchemaandresponseMimeTypeconfigurations using the@google/genaiv2.5.0 SDK. - OpenAI: Enforced via structured
response_formatJSON schema parameters. - Anthropic: Parsed and validated via tool-calling wrappers or dynamic JSON validation layers.
- DeepSeek: Guided by custom system instructions and validated against strict schemas downstream.
This guarantees consistent downstream JSON formatting regardless of the active AI_PROVIDER.
Publish & Serve
| Step | Action | Guard |
|---|---|---|
| Generate | generateLegalDocAction(type, locale) | requireAdmin() + Zod |
| Publish | publishLegalDocAction(docId) | requireAdmin() — auto-unpublishes prior version of same type + locale |
| Serve | GET /api/legal?type=privacy_policy&locale=en | edgeRateLimit(ip) — public, no auth, Cache-Control: s-maxage=3600 |
Supported types: privacy_policy, terms_of_service, cookie_policy, refund_policy
Context: LegalDocContext { companyName, companyUrl, supportEmail, jurisdiction } sourced from config.app.
Schema parity is mandatory: both lib/db/schema/legal.ts and supabase/schema.sql use the same supported type values. generated_legal_docs is unique on type + locale + version, allowing multiple versions per locale while preventing duplicate version rows.
Locale and Type Isolation
Publishing transitions are strictly hardened to isolate workflows by locale and type. When executing publishLegalDocAction(docId), the system validates that only the previous active document of that exact type and locale combination is unpublished, preventing any accidental cross-locale unpublishing or document bleeding.
Webhook Processing
Universal Pattern
export async function POST(request: NextRequest) {
const body = await request.text();
const headers = Object.fromEntries(request.headers);
const provider = getPaymentProvider();
const event = await provider.handleWebhook(body, headers);
// HMAC-SHA256 inside handleWebhook()
// Idempotency: provider.handleWebhook() claims webhook_logs before processing
await syncSubscriptionToDb(event);
return NextResponse.json({ received: true });
}Idempotency
successrows for the sameprovider+event_idare skipped- Active
processingrows newer than 5 minutes are skipped failedrows and staleprocessingrows are reused withattempts + 1- Handler executes only after the row is claimed
- Success/failure is recorded with the current attempt count
Duplicate deliveries = zero duplicate mutations, while retryable failures remain recoverable.
Billing state also uses provider-resource uniqueness. syncSubscriptionToDb()
upserts on (provider, provider_subscription_id), while savePurchase() treats
duplicate (provider, provider_payment_id) conflicts as idempotent success and
returns the existing purchase ID.
Creem webhook events must carry a durable event.id; missing IDs are rejected
instead of replaced with generated timestamp IDs.
Paid Product Name Mapping
mapProductNameToTier() is a paid-provider fallback only. When metadata tier IDs and configured price/product IDs do not identify the tier, it matches paid tier IDs/display names from PRICING_TIERS, maps config-driven highIntentAliases (e.g., business, agency, enterprise) to the correct paid tier, and falls back to the lowest paid tier. It emits a [mapProductNameToTier] warning whenever this fallback is used. Do not change this fallback to getDefaultTierId() unless the product event is explicitly non-tier-granting, because that would under-grant a completed paid purchase.
Data Export Patterns
Centralized CSV Export
Administrative modules requiring data export (e.g., ContactsTable, NewsletterTable, TransactionsTable) must use the centralized exportToCsv helper from lib/utils:
- Header Generation: Define a dictionary mapping data keys to human-readable headers.
- Streaming: The utility uses a Blob-based stream with
data:text/csv;charset=utf-8. - Escaping: The utility safely surrounds all string values with double quotes and escapes internal quotes to prevent CSV injection.
- BOM: Prepend
\uFEFF(Byte Order Mark) for Excel compatibility.
import { exportToCsv } from "@/lib/utils";
// Inside a component or event handler:
const handleExport = () => {
exportToCsv({
filename: "transactions_export.csv",
data: transactions,
headers: {
id: "Transaction ID",
amount: "Amount",
status: "Status",
},
});
};️ Advanced Database Patterns
Serializable Transactions
For high-integrity operations where a TOCTOU (Time-of-Check-Time-of-Use) race condition would cause data loss or corruption, use serializable isolation.
Example (Affiliate Payout):
The action validates config.affiliate.minimumPayoutThresholdInCents and writes the stored payout method/email into the payout record.
In requestPayout(), the sequence of reading pendingEarnings → updating earnings to zero → inserting a payout record is wrapped in a serializable transaction. This ensures that concurrent credits from webhooks do not get lost in the read/write window.
Organization invitation acceptance uses the same isolation level for the capacity check, member insert, profile update, and invitation status update. This prevents concurrent invite accepts from exceeding the owner's seat limit.
await db.transaction(
async (tx) => {
// 1. Read inside the serializable snapshot
// 2. Perform atomic updates
// 3. Insert record
},
{ isolationLevel: "serializable" },
);Unique Violation Retries
For random-string identifiers (like referral codes) where collisions are statistically possible but rare, use a retry loop to catch PostgreSQL code 23505.
let attempts = 0;
while (attempts < 3) {
try {
await db.insert(table).values({ code: generateCode() });
break;
} catch (error) {
if (error.code === "23505") attempts++;
else throw error;
}
}Background Jobs (Universal Adapter)
Vibe44 features a Universal Background Jobs Adapter that decouples background job handlers and cron triggers from the specific runtime/queue transport provider. The active provider is determined by the BACKGROUND_JOBS_PROVIDER environment variable (qstash or trigger).
Shared Job Handler Layer
All core business logic and background tasks are unified in lib/jobs/. Providers act strictly as transport delegates:
lib/jobs/: Central location for the actual background tasks (session cleanup, WebAuthn challenge purges, trial reminders, usage resets, weekly digests).lib/jobs/config.ts: Decoupled pure configuration and provider validation logic. This database-free module allows dynamic system liveness checks (e.g.lib/system/health.tsor/api/health) to validate background job configuration and connectivity without triggering database-bound handler side effects or dynamic imports.- No Business Logic Duplication: Provider-specific folders (
lib/qstash/andlib/trigger/) contain ONLY thin wrappers that invoke the unifiedlib/jobs/handlers.
Transport Adapters
-
QStash (Upstash)
- Ingress Endpoint:
/api/qstash/route.ts- parses and verifies Upstash webhook signatures in production. - Dispatcher: Enqueues and schedules jobs using the QStash HTTP client.
- Ingress Endpoint:
-
Trigger.dev (v4)
- Runner: Runs tasks asynchronously via the Trigger.dev v4 framework.
- Local Dev: Connects using
npx trigger.dev devalongside your local Next.js development server.
Safeguards
- XSS —
escapeHtml()sanitizes names before HTML injection - Validated delivery —
result.value.successchecked before stats - Dynamic pricing — plan names from
getPricingTier(), never hardcoded - QStash Security — Local development bypass is restricted by
QSTASH_ALLOW_DEV_BYPASS,isDevMode(), and absence ofVERCEL_ENV. Production requires Upstash signature verification. Verified bodies are then narrowed with Zod; every task must include a non-emptytypediscriminator and may carry handler-owned fields.
Health Check
GET /api/health and public oRPC health are intentionally minimal and public:
{
"status": "healthy",
"timestamp": "2026-04-28T00:00:00.000Z",
"version": "1.0.0",
"uptime": 123.45
}Detailed dependency diagnostics, including Redis and payment readiness, belong
to authenticated super-admin-only health surfaces. Payment readiness comes from
lib/payments/readiness.ts and reports missing variable names without exposing
secret values.
System Metrics (systeminformation)
For advanced performance monitoring, the backend integrates the systeminformation package. This allows super-admins to view detailed hardware diagnostics, CPU load, memory utilization, and network statistics directly from the dashboard, facilitating proactive infrastructure scaling.
Copyright © 2026 Vibe44. Built with ❤️ by Scale with Alap.