hubODSEA
Tech StackMay 26, 2026•24 min read

The Production AI Stack We Actually Use: LangGraph + Next.js 15 + Supabase + Vercel

Seven Architecture Decision Records from ODSEA's real production stack — Bun over Node, Next.js 15 App Router over Remix, Supabase over Firebase, Drizzle over Prisma, LangGraph over CrewAI, Infisical over Doppler, Vercel over Railway. What we chose, what we rejected, and the trade-offs we accepted.

Alex Chen

Alex Chen

CTO & Co-Founder

The Production AI Stack We Actually Use: Seven Architecture Decision Records from 2026

Every engineering team has opinions about their stack. Most of those opinions were formed in conference talks or vendor blog posts, not in production incidents at 2 AM. This post is different: it covers the stack ODSEA actually runs in production today, written in Architecture Decision Record format — the tool that software teams use to document not just what they decided, but why, what they considered and rejected, and what they gave up in the process.

Why We Document Architecture Decisions Publicly

Architecture Decision Records exist because software teams keep repeating the same mistakes. A new engineer joins, sees an unusual pattern, "fixes" it without understanding why it was there, and six weeks later you're debugging the exact failure mode the original decision was designed to prevent. ADRs solve this by preserving the reasoning, not just the outcome.

We document ours publicly for a different reason: most "tech stack" posts on the internet are marketing. They describe tools as if every choice is obviously correct, every migration painless, every trade-off invisible. The result is a generation of engineering teams that copy stacks they read about rather than thinking through their own constraints.

Our stack is the result of roughly two years of production operation across a platform that serves thousands of users, processes AI agent workflows 24 hours a day, manages a database with over 200 tables and 16 schema files, and deploys multiple times per week. The choices documented below were not made lightly, and some of them are wrong for your situation. Read them that way.

The stack at the time of writing (May 2026): Bun 1.3.13 as runtime and package manager, Next.js 15 with App Router, Supabase (PostgreSQL) for database and auth, Drizzle ORM for schema and migrations, LangGraph for AI agent orchestration, Infisical for secrets management, and Vercel for deployment.

ADR-001: Bun over Node.js

Status: Adopted
Context: We needed a JavaScript/TypeScript runtime and package manager. The project started on Node.js 20 with pnpm. During a platform performance review in late 2024, cold start times on Vercel and local development iteration speed emerged as notable pain points.

Considered: Node.js 22 (LTS) with pnpm, Node.js 22 with npm, Deno 2, Bun 1.x

Decision: Bun 1.x (currently pinned to bun@1.3.13)

Rationale:

Bun's headline numbers are real in practice, not just in synthetic benchmarks. On our monorepo (approximately 1,200 packages in the dependency tree), bun install completes in roughly 8 seconds on a warm cache. The equivalent pnpm install on the same machine took 47 seconds. This matters more than it sounds: every CI run, every container build, every developer environment setup runs faster.

Bun's native TypeScript support eliminates a category of tooling. We no longer run ts-node, tsx, or any transpilation layer to execute TypeScript scripts — Bun executes .ts files natively. The entire ~/ scripts directory in our repository runs directly as TypeScript:

# Execute TypeScript script directly — no build step, no tsx, no ts-node
bun run ~/factory-intake-runner.ts

# Development server with Turbopack — Bun handles TS, Turbopack handles bundling
bun run next dev --turbopack

Bun is also faster at runtime for I/O-heavy workloads. Our batch processing scripts that call Supabase, process AI responses, and write files back showed 20–30% wall-clock improvement switching from Node.js to Bun with no code changes. For pure CPU work the difference is smaller, but we don't have much of that.

What we gave up: Ecosystem certainty. Bun's Node.js compatibility is excellent but not perfect. We encountered two subtle issues during migration: one native module that required a Bun-compatible shim, and a test runner compatibility issue that required running Vitest through Bun with specific configuration flags. Neither was a showstopper, but they cost a day of debugging.

When we'd reconsider: If a critical dependency becomes permanently incompatible with Bun and no workaround exists. Node.js 22 is a perfectly reasonable runtime — Bun is just measurably faster for our workload profile.

The rule we enforce: Never edit package.json manually. All package changes go through bun add or bun remove. Manual edits break the lockfile in ways that are expensive to diagnose.

ADR-002: Next.js 15 App Router over Pages Router, Remix, and Astro

Status: Adopted
Context: We needed a web framework capable of server-side rendering for SEO, React Server Components for performance, streaming responses for AI-generated content, and clean integration with Supabase. We evaluated this decision during the initial platform design in 2024 and again when Next.js 15 stabilized the App Router API.

Considered: Next.js 15 (App Router), Next.js 14 (Pages Router), Remix 2, Astro 4, SvelteKit

Decision: Next.js 15 with App Router

Rationale:

React Server Components are the most significant architectural change to React since hooks. Next.js 15's App Router is the most mature production implementation of RSC, and the capability gap between it and alternatives has widened, not narrowed, over 2025–2026.

The practical advantage in our architecture: Server Components fetch data directly from Supabase with zero client-side round trips. There is no intermediate API layer for read operations. The pattern looks like this:

// Server Component — runs on the server, fetches directly from Supabase
// No useEffect, no loading states, no client-side fetch
import { createClient } from '@/lib/supabase/server'

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  
  const { data: projects } = await supabase
    .from('projects')
    .select('*')
    .eq('owner_id', user.id)
  
  return <ProjectList projects={projects} />
}

The await createClient() pattern is mandatory in server components and server actions — not optional. It correctly handles cookie-based session management in the Next.js 15 async context model. This matters because the App Router changed how cookies are accessed between Next.js 14 and 15, and projects that didn't migrate the auth pattern correctly hit subtle session bugs in production.

Streaming is the second major advantage for our AI use case. When a multi-agent pipeline is generating content, we stream the response progressively rather than waiting for the entire LLM output. React's Suspense boundaries, combined with Next.js 15's streaming infrastructure, let us show partial results immediately:

// AI content page streams results as they arrive from the agent
export default async function ContentPage() {
  return (
    <Suspense fallback={<ContentSkeleton />}>
      <AIGeneratedContent />  {/* Streams as LLM produces output */}
    </Suspense>
  )
}

Turbopack (enabled via bun run next dev --turbopack) reduced our local development HMR from 2–4 seconds to under 300ms on a 150,000-line codebase. That is a real developer experience improvement, not a marketing number.

What we gave up: Remix's form-first progressive enhancement model is architecturally elegant. The action / loader pattern is more predictable than Next.js server actions for complex form workflows. We chose Next.js because the ecosystem depth — community knowledge, third-party integrations, the Vercel deployment story — is substantially deeper than Remix. For a product team building business applications rather than content sites, ecosystem depth matters more than architectural purity.

Astro was compelling for the marketing site specifically, but managing two frameworks across the product and marketing codebases introduces cognitive overhead that the performance gain does not justify at our current scale.

Production incident we did not anticipate: Next.js 15 changed caching defaults significantly compared to 14. In Q4 2025, a production deployment served stale exam question data to teachers for approximately 45 minutes after a database update, because page caches were not being invalidated correctly. The fix required explicit revalidatePath and revalidateTag calls after database writes — behavior that was automatic in pages that had assumed implicit revalidation. Retrofit cost: two days of engineering. This is a real operational cost of the App Router migration.

When we'd reconsider: If React Server Components were superseded by a better model, or if the Turbo/Vercel alignment created pricing lock-in that became unacceptable. Neither seems imminent.

ADR-003: Supabase over Firebase, Neon, and PlanetScale

Status: Adopted
Context: We needed a database solution providing managed PostgreSQL with excellent developer tooling, a production-ready auth system that handles OAuth and JWT without custom infrastructure, real-time subscription capabilities for collaborative features, and Row Level Security for data isolation in a multi-tenant product.

Considered: Supabase, Firebase (Firestore + Firebase Auth), Neon (serverless Postgres), PlanetScale, self-managed Postgres on Railway or Render

Decision: Supabase

Rationale:

Supabase bundles five capabilities we need into a single service: managed PostgreSQL, connection pooling via pgBouncer (critical for Vercel's serverless environment), authentication (JWT, OAuth, magic links, SSO), Row Level Security for database-level authorization, and real-time subscriptions via WebSocket. The alternative is running and maintaining four to five separate services.

The pgBouncer configuration is worth highlighting specifically because it solves a concrete Vercel + PostgreSQL problem that is not obvious until you hit it. Vercel's serverless functions open new database connections on every invocation. Direct PostgreSQL connections are expensive to establish and there is a hard cap. pgBouncer pools those connections, making serverless + Postgres viable:

// drizzle.config.ts — connection uses Supavisor pooler URL for Vercel deployments
// Direct connection URL = expensive per-function connection overhead
// Supavisor connection pooler URL = pool shared across all serverless functions
const connectionString = process.env.DATABASE_URL  // Points to Supavisor in prod

Row Level Security is the most underappreciated feature in our stack. Defining data access rules at the database layer means they apply regardless of which application endpoint accesses the data, which AI agent executes a query, or which background worker connects to the database. In a multi-agent system where 15+ specialized agents read and write the same tables, application-level access control is not reliable enough — RLS provides a second enforcement layer that cannot be bypassed by agent bugs:

-- Students can only read their own exam submissions
CREATE POLICY "students_read_own_submissions"
ON exam_submissions
FOR SELECT
USING (auth.uid() = student_id);

-- Agents using the service role key bypass RLS by design
-- Only the agent-platform service is issued a service role key

What we rejected and why:

Firebase: Firestore's document model would have required significant data modeling compromises for our relational schema (200+ tables with complex foreign key relationships). Firebase Auth is excellent, but the proprietary ecosystem creates migration risk that an open-source PostgreSQL stack does not. Firebase is the right choice for mobile-first apps with simple data models; it is the wrong choice for education platforms with grade books, lesson plans, exam structures, and payment records.

Neon: Neon's serverless branching feature is genuinely compelling for development workflows, and their cold start times have improved substantially. We evaluated Neon seriously in late 2024. The decision came down to auth and real-time: Neon is a database, not a platform. We would have needed to run Clerk or Auth0 for auth and a separate real-time layer. The operational complexity of managing three services versus one was the deciding factor.

PlanetScale: PlanetScale's horizontal scaling story is better than Supabase for extreme read volumes — millions of reads per second, sharded globally. We are not at that scale, and PlanetScale's removal of their free tier in 2024 and subsequent pricing changes made it harder to justify for a platform that needed a development environment at low cost.

Real production numbers from our current deployment:

  • Database: approximately 40GB of data, 200+ tables across 16 schema files
  • Active connections via pgBouncer: peak ~180, average ~60
  • Supabase Pro: $25/month for this workload
  • Auth: ~12,000 MAU
  • Migrations: managed through Drizzle ORM, applied at Vercel build-time on deploys to main

The cost efficiency relative to the capability is the most defensible argument. An equivalent self-managed stack — a managed RDS instance, Clerk for auth, a Pusher subscription for real-time, pgBouncer deployed separately — would cost more and require maintenance engineering.

When we'd reconsider: If we exceed Supabase's connection pooler capacity at scale, or if the open-source/hosted parity that makes Supabase's migration story compelling deteriorates. Supabase's open-source nature means a self-hosted migration is always theoretically possible — that exit ramp matters when evaluating infrastructure vendor risk.

ADR-004: Drizzle ORM over Prisma and Raw SQL

Status: Adopted
Context: We needed a TypeScript ORM that could manage schema migrations reliably across a database with 200+ tables, integrate cleanly with Bun, generate lightweight query overhead suitable for serverless execution, and support a team workflow where multiple developers change schema concurrently without migration conflicts.

Considered: Drizzle ORM, Prisma 5, Kysely (type-safe SQL builder), raw SQL with typed queries

Decision: Drizzle ORM

Rationale:

Drizzle's architecture is fundamentally different from Prisma in one way that matters for serverless deployments: it has no query engine. Prisma runs a separate binary (the Prisma Client query engine) that manages the database connection and translates Prisma's intermediate representation to SQL. This binary adds cold start overhead in serverless environments that has been a persistent operational complaint against Prisma on Vercel.

Drizzle is just TypeScript — no binary, no runtime process, no cold start penalty. The client connects directly to the database, generates SQL, and executes it. On Vercel, this translates to measurably faster first-request times for serverless functions.

The schema definition model is the second advantage. Prisma uses its own schema language (.prisma files) that sits outside the TypeScript project. When you rename a field in a Prisma schema, TypeScript's refactoring tools don't automatically update references — you have to run prisma generate to regenerate the client, then fix TypeScript errors manually. Drizzle schemas are TypeScript:

// packages/db/src/schema.ts — Drizzle schema in TypeScript
// Renaming a field here propagates through TypeScript's type system immediately
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core'

export const examQuestions = pgTable('exam_questions', {
  id: uuid('id').primaryKey().defaultRandom(),
  content: text('content').notNull(),
  subjectId: uuid('subject_id').references(() => subjects.id),
  difficulty: integer('difficulty').notNull().default(1),
  createdAt: timestamp('created_at').defaultNow(),
})

// Type is inferred automatically — no code generation step
type ExamQuestion = typeof examQuestions.$inferSelect

The migration workflow that governs our entire database change process:

# 1. DatabaseDev modifies schema.ts (or one of the 15 other schema files)
# 2. Generate migration — custom name makes the migration history readable
bun run drizzle-kit generate --custom --name=add_exam_difficulty_index

# 3. Apply to development database — secrets injected by Infisical, never hardcoded
infisical run --env=dev -- bun run drizzle-kit migrate

# 4. Commit the generated migration file alongside the schema change
# 5. Production migration runs automatically at Vercel build-time:
#    vercel-build: "bun run drizzle-kit migrate --config=drizzle.vercel.config.ts && ..."

The Drizzle config at drizzle.config.ts lists all 16 schema files explicitly. This is intentional: a wildcard import would silently include schema changes from feature branches that aren't ready to ship. Explicit listing means you have to consciously add new schema files to the migration config.

The rule we cannot compromise on: Only @DatabaseDev runs drizzle-kit generate and commits files under packages/db/src/migrations/. No other agent, developer, or automated process generates migration files. The reason: if two developers independently run drizzle-kit generate for different schema changes, both migrations get the same timestamp-based filename, and whichever lands in main second will cause a migration history collision. One committed migration file per schema change, one author responsible.

What we gave up: Prisma's developer experience polish is higher. Prisma Studio (the GUI for browsing database data) has no Drizzle equivalent. Prisma's error messages are more helpful for newcomers. If we were building a project with a small, stable schema and a junior team, Prisma's guardrails would be worth the trade-offs. At 200 tables with frequent schema evolution, Drizzle's TypeScript-native approach and serverless performance win.

When we'd reconsider: If Drizzle's API significantly regresses, or if Prisma eliminates its query engine overhead and adds TypeScript-native schema definition. Neither is imminent.

ADR-005: LangGraph over CrewAI and AutoGen

Status: Adopted
Context: ODSEA's core AI product is a multi-agent system that processes educational content through sequential pipelines involving 15+ specialized agents: researchers, writers, reviewers, testers, database developers, and orchestrators. We needed a framework that could handle stateful, long-running workflows, recover from partial failures, support human-in-the-loop approval gates, and run reliably in a production environment — not just in demos.

Considered: LangGraph, CrewAI, AutoGen (Microsoft), LangChain LCEL chains, custom direct LLM orchestration

Decision: LangGraph

Rationale:

State persistence is the most important capability criterion for production agent workflows, and LangGraph is the only framework in our evaluation set that treats it as a first-class concern rather than an afterthought.

The StateGraph + Checkpointer combination gives us complete workflow execution state that survives process restarts, server deployments, and infrastructure failures. When a 45-minute multi-agent pipeline fails at step 32 of 40, LangGraph resumes from step 32. Without checkpointing, the entire pipeline restarts from step 1 at a cost of wasted LLM tokens, wasted time, and a user experience that looks broken:

from langgraph.graph import StateGraph
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver

# State persisted to Supabase Postgres — survives process restarts and deployments
checkpointer = AsyncPostgresSaver.from_conn_string(os.getenv("DATABASE_URL"))

workflow = StateGraph(PipelineState)
workflow.add_node("research", research_agent)
workflow.add_node("draft", writer_agent)
workflow.add_node("review", reviewer_agent)
workflow.add_node("human_approval", human_approval_gate)

# Execution pauses here and waits for external input before continuing
workflow.add_interrupt_before("human_approval")

app = workflow.compile(checkpointer=checkpointer)

# Resume a paused workflow from where it stopped
result = await app.ainvoke(
    Command(resume={"approval": "approved", "reviewer_id": user_id}),
    config={"configurable": {"thread_id": workflow_id}}
)

The human-in-the-loop interrupt mechanism was the decisive capability that eliminated alternatives. Our pipelines include explicit human approval gates — a generated exam paper flagged for teacher review, an AI-drafted response requiring editorial sign-off, a financial calculation requiring human verification before sending to a client. LangGraph's interrupt/resume mechanism handles this production requirement correctly. CrewAI and AutoGen do not offer an equivalent that works reliably in a web application context.

Why we rejected CrewAI: CrewAI's YAML-based agent configuration is appealing for simple pipelines, and the learning curve is lower than LangGraph. In a proof-of-concept, CrewAI's 50-line agent definition feels much better than LangGraph's 300-line workflow definition for the same use case. The problem appears in production: CrewAI lacks granular checkpoint control, making failure recovery unpredictable. We also found that "agent autonomy" in CrewAI — where agents decide what to do next without explicit graph edges — produces inconsistent behavior in production that is difficult to debug or monitor.

Why we rejected AutoGen: AutoGen (Microsoft) is architecturally similar to CrewAI in the relevant ways. The conversational agent model is excellent for research and exploration tasks. For deterministic production pipelines with explicit approval gates and compliance requirements, AutoGen's non-determinism is a liability, not a feature.

The trade-off we accepted: Development velocity. LangGraph's lower-level API means substantially more code per workflow than CrewAI or AutoGen. Our average pipeline definition is 400–600 lines of Python. The CrewAI equivalent of the same workflow would be 60–100 lines. We accept this trade-off permanently, because production reliability is not negotiable for a platform that teachers and students depend on daily.

2026 update: LangGraph Cloud launched a hosted deployment and monitoring service in late 2025 which we adopted in Q1 2026. It eliminated our self-managed LangGraph infrastructure (a Docker-based Postgres + Python service). The monitoring UI — showing real-time workflow state, interrupt queues, and checkpoint history — is worth the subscription cost for an operations team managing pipelines this complex.

When we'd reconsider: If a competing framework achieves LangGraph-level state persistence and interrupt handling with significantly less code overhead. The 5–10x verbosity gap is real, and we monitor the ecosystem actively.

ADR-006: Infisical over Doppler and .env Files

Status: Adopted
Context: We operate across three environments (development, staging, production) with 40+ secrets per environment — database URLs, API keys for Claude/GPT/Supabase/Vercel/Cloudflare, service-to-service tokens, and infrastructure credentials. We needed a secrets management solution that prevented accidental credential exposure, supported team rotation without requiring every developer to update local files, and provided an audit trail for compliance purposes.

Considered: Infisical, Doppler, AWS Secrets Manager, HashiCorp Vault, Vercel environment variables only, .env files in version control (rejected immediately)

Decision: Infisical

Rationale:

The .env pattern has a fundamental security problem: credentials that live in files will eventually end up in version control, in shell history, in screenshot-based bug reports, and in Slack messages. This is not a team discipline problem — it is a tooling design problem. Infisical solves it by making the secure path the easy path:

# Development: secrets injected at process start, never written to disk
infisical run --env=dev -- bun run next dev --turbopack

# Run database migration with dev credentials
infisical run --env=dev -- bun run drizzle-kit migrate

# TypeScript script that needs API keys
infisical run --env=dev -- bun run ~/factory-research-start.ts

# NEVER: infisical run --env=prod -- [any mutation operation]
# Production writes only through Vercel build-time (merges to main branch)

The infisical run --env=[environment] pattern injects secrets as environment variables for the duration of the child process. They are not written to .env files, they do not appear in shell history (because the infisical command itself does not log them), and they are never present on disk. A developer laptop with Infisical access has no persistent credentials that can be exfiltrated.

Team rotation is the operational advantage. When an API key is compromised or rotated, a single update in the Infisical dashboard propagates to every developer and every CI job immediately. With .env files, key rotation requires every developer to update their local file, which in practice takes days and creates a window where some processes are still using the old key.

The safety rule we enforce without exception: infisical run --env=prod is read-only. We never use production credentials to run scripts that write to the production database or modify production infrastructure. Production mutations happen only through the Vercel build pipeline triggered by merging to main. This rule is documented in the agent system and enforced in code review. Violating it is a production incident, not a warning.

What we gave up: Doppler's UI is more polished and their onboarding is faster. AWS Secrets Manager integrates more naturally with AWS-native infrastructure. We are not AWS-native, and Doppler's pricing at team scale is slightly higher than Infisical's comparable tier. The functional difference between Infisical and Doppler at our scale is small — either would be a defensible choice.

When we'd reconsider: If Infisical's self-hosted option or cloud service had a significant security incident that eroded trust in the platform, we would migrate to Doppler. The migration cost is low — the infisical run --env=[env] -- [command] invocations are straightforward to replace with doppler run --config=[env] -- [command].

ADR-007: Vercel over Netlify, Railway, and Self-Hosted

Status: Adopted
Context: We needed a deployment platform for a Next.js 15 application that handled CI/CD automatically from GitHub, provided edge functions globally for authentication middleware, supported preview deployments for every pull request, and managed production and staging environments from different Git branches without deployment scripts.

Considered: Vercel, Netlify, Railway, Fly.io with Docker, self-hosted on a VPS (Hetzner/DigitalOcean)

Decision: Vercel

Rationale:

Vercel's Git-based deployment model matches our development workflow with zero configuration. The branch-to-environment mapping is the most operationally important feature:

Git BranchEnvironmentURL
mainProductionbangioi.vn
dev + feature branchesStaging / Previewbangioi.vercel.app

Every pull request gets an isolated preview deployment automatically. Every merge to dev redeploys staging. Every merge to main redeploys production with database migrations. This is the correct behavior for a team deploying multiple times per week, and it requires zero deployment scripts or configuration to maintain.

The Next.js-native integration is the second advantage. Vercel built Next.js. Incremental Static Regeneration, edge middleware, streaming, image optimization, and server actions all work on Vercel without configuration that would otherwise require manual nginx/CDN setup. The integration quality is measurably higher than any other platform.

Edge middleware is particularly valuable for our use case. Authentication checks and locale detection run at the edge before any regional server processes the request — eliminating a full round-trip for the most common user interactions:

// middleware.ts — runs at Vercel edge globally, before any server handles the request
export async function middleware(request: NextRequest) {
  const supabase = createServerClient(...)
  const { data: { user } } = await supabase.auth.getUser()
  
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

The rule we enforce: Never run vercel deploy or npx vercel deploy from local. All deployments must originate from a GitHub push. This prevents the classic Vercel mistake: deploying local source code that includes uncommitted local changes, debugging secrets, or environment-specific modifications that are not in version control and will not survive the next deploy.

What we gave up: Cost. Vercel Pro is $20/month per member, and compute pricing at high traffic volumes can escalate in ways that Railway and self-hosted cannot. Cloudflare Pages would serve the same workload for significantly less money. The trade-off: Cloudflare Pages' Next.js support is incomplete (some App Router features require workarounds), and the developer experience overhead of managing a less integrated platform costs engineering time that is more expensive than the compute savings at our current scale.

Railway is an excellent platform for backend services and databases. We use it for supplementary services outside the main Next.js application. For the primary Next.js deployment, Vercel's integration depth is not matched.

When we'd reconsider: At a traffic volume where Vercel's compute costs exceed the engineering cost savings of the deployment integration — typically in the range of 10+ million monthly page views. We are not there. If we were, the migration path would be to Cloudflare Pages with Workers for the edge functions.

The Full Picture: How It All Connects

These seven decisions don't exist in isolation — they are a system. The connections between them are where the real architectural value lives.

The development loop: A developer changes a database schema (drizzle.config.ts reads 16 schema files across packages/db/src/), generates a migration with bun run drizzle-kit generate, applies it locally using infisical run --env=dev -- bun run drizzle-kit migrate, implements the feature in a Next.js Server Component with await createClient() for Supabase access, and pushes to a feature branch. Vercel creates a preview deployment automatically, and the migration runs at build-time against the staging database.

The AI pipeline loop: An incoming content processing request triggers a LangGraph workflow. The workflow state is checkpointed to Supabase PostgreSQL — the same database the Next.js application reads from. When the pipeline reaches a human approval gate, it persists state and sends a notification. The Next.js dashboard loads the pending approval over a direct Server Component → Supabase query (no API layer). The teacher approves, which calls a Server Action, which resumes the LangGraph workflow via an API call to the agent platform. All secrets (LangGraph API keys, Claude/GPT API keys, Supabase service role key) are injected by Infisical — never stored in the codebase.

The deployment loop: A merge to main triggers Vercel's build process. The build script runs bun run drizzle-kit migrate --config=drizzle.vercel.config.ts against the production database using the Supavisor connection pooler URL (required for IPv4 compatibility in Vercel's serverless environment), then builds the Next.js application. If migration fails, the build fails and production is protected. If the build passes, the new deployment goes live at bangioi.vn.

The architecture can be visualized as three concentric layers: the data layer (Supabase PostgreSQL + Drizzle schema + RLS policies), the application layer (Next.js 15 App Router + Bun runtime + Vercel edge), and the intelligence layer (LangGraph agent platform + Claude/GPT models + Infisical secrets). Each layer communicates with the one below it through well-defined interfaces, and Infisical sits outside all three layers, injecting credentials at runtime without storing them anywhere in the stack.


This stack is the result of two years of production operation. Every tool here was chosen because it solved a real problem better than its alternatives — not because it was trending or because a vendor offered an incentive. The decisions above document what we chose, why we chose it, and what we gave up. Your constraints are different from ours, and some of these choices would be wrong for your situation.

If you're evaluating a similar stack and want to talk through the specific trade-offs for your scale, team size, and domain, we're available for that conversation. The architecture decisions that matter most are the ones you make before you start building, not after you've already deployed.

Our AI agents service covers the agent architecture layer — LangGraph, state design, multi-model routing, and human-in-the-loop patterns — in considerably more depth for teams specifically evaluating AI orchestration frameworks for production systems.

LangGraphNext.jsSupabaseVercelProduction ArchitectureBunDrizzle ORMInfisicalArchitecture Decision Record

Related Articles