0%
Vibe44 Logo

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:

  1. Drizzle reads project explicit columns. Bare .select() is prohibited in actions, API routes, background handlers, and webhook administration.
  2. 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.
  3. Webhook administration reads use WEBHOOK_LOG_LIST_PROJECTION and WEBHOOK_LOG_RETRY_PROJECTION; retry and list paths never hydrate unused webhook columns.
  4. Background subscription/trial reminder jobs load only email, fullName, and subscriptionTier from profiles.
  5. Independent async reads use Promise.all() unless one result is required to construct another query.

Action Inventory

FileActions
user.tsProfile CRUD, avatar upload, account deletion with cascade
billing.tsCheckout, portal, credit deduction, subscription sync
organization.tsOrg CRUD, invite/remove/role change (barrel export)
projects.tsProject CRUD with org or user ownership
super-admin.tsSuper-admin user management, impersonation, system health (barrel export)
support-tickets.tsTicket CRUD, replies, status transitions, archive/unarchive/delete lifecycle, AI canned responses
api-keys.tsKey generation (SHA-256), revocation, usage tracking
dashboard.tsSSR-safe data fetching for metrics, charts, and activity logs with mock fallback
super-admin/legal.tsAI legal doc generation, version control, publish/unpublish
contact.tsContact form submission, super-admin status management, internal notes
affiliate.tsRegistration, referral tracking, payouts
waitlist.tsPre-launch signup
newsletter.tsNewsletter subscription, super-admin CRUD, source tracking
onboarding.tsGoal selection, profile completion tracking
mfa.tsTOTP enrollment, challenge, unenrollment
passkeys.tsWebAuthn registration + login
impersonation.tsSuper-admin user impersonation session management
super-admin/_shared.tsInternal 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 lastUsedAt to 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 profiles table.
  • Implementation: Super-Admin views perform a relational lookup to fetch the user's avatar_url, full_name, and username.
  • 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:

  1. Selection: Filtered data is retrieved based on the current dashboard state (search/sort).
  2. Transformation: JSON records are converted to CSV strings with double-quote escaping for all fields.
  3. Encoding: Prepend \uFEFF (Byte Order Mark) to ensure proper UTF-8 character handling in Excel/Numbers.
  4. Delivery: Blob-based download using the application/csv MIME 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:
    1. Billing: Cancels active subscriptions at the payment provider.
    2. Storage: Deletes all user-owned files under the user prefix from every configured Supabase Storage bucket.
    3. Logs: Purges user-scoped activity logs while writing a retained, privacy-minimized auth_events.account_deleted audit event.
    4. Marketing: Removes the user's email from waitlist_subscribers to ensure PII cleanup.
    5. Auth: Deletes the Supabase Auth user record via the service_role super-admin client.
    6. Profile: Cascades from the auth.users deletion 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
ProcedureAuthUse Case
publicProcedureNoPublic, aggregate health
protectedProcedureYesUser data
superAdminProcedureYes + Super-AdminSuper-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:

  1. Route classification — public / auth / protected / super-admin / API
  2. Rate limiting — Upstash Redis, per-tier from lib/config/pricing.ts
  3. Circuit breaker — auto IP ban after 5 violations
  4. CSP nonce — per-request, strict-dynamic
  5. CSRF — HMAC-bound double-submit cookie + user binding; authenticated page requests seed/refresh __csrf_token; root CSRFProvider exposes csrfFetch() to public and protected client surfaces; authenticated support chat and invite acceptance require CSRF, guest chat is allowed only when no session exists
  6. Session refresh — Supabase token rotation
  7. MFA enforcement — AAL2 check, redirect if not met (fail-closed on error)
  8. Feature flags — disabled modules → /dashboard
  9. Header injection — x-pathname, x-org-id, x-nonce, x-impersonating-*
  10. Impersonation context — verifies signed cookie, 1-hour auto-expiry tracking, and read-time super-admin/target revalidation
  11. Locale negotiation — Accept-Language detection + NEXT_LOCALE cookie 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

GroupPatternBehavior
Public/, /pricing, /blog/*No auth, rate limited
Auth/login, /signup, /forgot-*Redirect to dashboard if logged in
Protected/dashboard/*, /dashboard/billingRedirect 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 include X-CSRF-Token when the middleware-seeded CSRF cookie exists; mutating browser fetch() calls must use csrfFetch() 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.
RouteMethodPurpose
/api/auth/callbackGETOAuth, magic link, and password-reset code exchange (PKCE)
/api/healthGETMinimal public liveness metadata
/api/legalGETPublic published legal docs (rate-limited, CDN-cached)
/api/rpc/[...path]ALLoRPC typed API; public health is aggregate-only, dependency health is super-admin-only
/api/webhooks/stripePOSTStripe webhooks
/api/webhooks/polarPOSTPolar webhooks
/api/webhooks/lemon-squeezyPOSTLemon Squeezy webhooks
/api/webhooks/dodoPOSTDodo webhooks
/api/webhooks/creemPOSTCreem webhooks
/api/auth/passkey/register/*POSTWebAuthn registration
/api/auth/passkey/login/*POSTWebAuthn authentication
/api/chatPOSTAI chat (SSE streaming)
/api/support/voiceGET/POSTVoice catalog and TTS via ElevenLabs/Murf
/api/support/voice/sttPOSTSpeech-to-text via ElevenLabs; accepts multipart audio file
/api/support/transcribePOSTLegacy speech-to-text route; accepts multipart file
/api/super-admin/*ALLSuper-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-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 responseSchema and responseMimeType configurations using the @google/genai v2.5.0 SDK.
  • OpenAI: Enforced via structured response_format JSON 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

StepActionGuard
GenerategenerateLegalDocAction(type, locale)requireAdmin() + Zod
PublishpublishLegalDocAction(docId)requireAdmin() — auto-unpublishes prior version of same type + locale
ServeGET /api/legal?type=privacy_policy&locale=enedgeRateLimit(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

  1. success rows for the same provider + event_id are skipped
  2. Active processing rows newer than 5 minutes are skipped
  3. failed rows and stale processing rows are reused with attempts + 1
  4. Handler executes only after the row is claimed
  5. 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.

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:

  1. Header Generation: Define a dictionary mapping data keys to human-readable headers.
  2. Streaming: The utility uses a Blob-based stream with data:text/csv;charset=utf-8.
  3. Escaping: The utility safely surrounds all string values with double quotes and escapes internal quotes to prevent CSV injection.
  4. 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.ts or /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/ and lib/trigger/) contain ONLY thin wrappers that invoke the unified lib/jobs/ handlers.

Transport Adapters

  1. 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.
  2. Trigger.dev (v4)

    • Runner: Runs tasks asynchronously via the Trigger.dev v4 framework.
    • Local Dev: Connects using npx trigger.dev dev alongside your local Next.js development server.

Safeguards

  • XSS — escapeHtml() sanitizes names before HTML injection
  • Validated delivery — result.value.success checked 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 of VERCEL_ENV. Production requires Upstash signature verification. Verified bodies are then narrowed with Zod; every task must include a non-empty type discriminator 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.