Aplio is an internal recruiting and application platform. Admins and managers create positions with custom questions; applicants build a profile and submit applications; managers move applications through a review pipeline (draft → applied → reached_out → interview_scheduled → reviewing → accepted / rejected / withdrawn). The platform is designed for operational teams that need structured, configurable hiring workflows without the overhead of general-purpose ATS products.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, React 19, Turbopack) |
| Database / ORM | Prisma 7 (@prisma/adapter-pg) · Neon Postgres |
| Auth | Stack Auth via @neondatabase/auth |
| Styling | Tailwind CSS 4 · shadcn/ui (Radix, new-york style) |
| Language | TypeScript (strict) |
| Validation | zod 4 · react-hook-form |
| Notifications | sonner |
- Node.js 22+ LTS (see
@types/nodeinpackage.json) - npm (comes with Node)
- Docker (for a local Postgres instance via
docker-compose.yml) or a hosted Neon Postgres URL
cp .env.example .env.localOpen .env.local and fill in the four required variables:
| Variable | Description |
|---|---|
DATABASE_URL |
Postgres connection string (pooled). Local Docker: postgresql://admin:admin@localhost:5432/aplio |
DIRECT_URL |
Direct (non-pooled) connection string. Local Docker: same as DATABASE_URL |
NEON_AUTH_BASE_URL |
Stack Auth base URL from your Neon Auth project |
NEON_AUTH_COOKIE_SECRET |
Stack Auth cookie secret |
RESEND_API_KEY |
Resend API key for transactional email delivery |
RESEND_FROM_EMAIL |
Verified sender address in Resend (e.g. noreply@yourdomain.com) |
SKIP_WEBHOOK_VERIFICATION |
Set to true to bypass Ed25519 webhook signature verification in local development. Must not be set in production. |
Note: Prisma CLI commands (
prisma:migrate,prisma:seed) read from.env; Next.js reads.env.local. Both files are gitignored. For local development you can keep the same values in both.
npm cinpm run db:startThis spins up a Postgres 16 container (admin/admin, database aplio, port 5432). Skip this step if you are using a hosted Neon URL.
npm run prisma:migrate
npm run prisma:generatenpm run prisma:seednpm run devThe app runs at http://localhost:3000.
| Script | Description |
|---|---|
dev |
Start Next.js dev server with Turbopack |
build |
Generate Prisma client, then build for production |
start |
Run the production build |
prisma:generate |
Regenerate the Prisma client from prisma/schema.prisma |
prisma:migrate |
Create and apply a new migration (prisma migrate dev) |
prisma:seed |
Run the seed script at prisma/seed.ts |
db:start |
Start the local Postgres Docker container |
db:stop |
Stop the local Postgres Docker container |
db:reset |
Reset the database and re-run all migrations |
prettier:fix |
Format all files with Prettier |
prettier:check |
Check formatting (used in CI) |
eslint:check |
Lint with zero warnings allowed (used in CI) |
eslint:fix |
Auto-fix lint issues |
tsc:check |
Type-check without emitting (used in CI) |
Always run Prisma through
npm run prisma:*— nevernpx prismadirectly.
Preview databases come from the Neon Previews Integration (installed on the Vercel project) — it owns per-PR branch creation, DATABASE_URL/DATABASE_URL_UNPOOLED injection, and branch teardown. There is no bespoke workflow for this anymore.
Preview migrations run inside the Vercel build itself, driven by vercel.json's buildCommand: when VERCEL_ENV is preview, it runs prisma migrate deploy against DATABASE_URL_UNPOOLED (required — the pooled endpoint rejects migrate deploy) before the normal build. On Production and Development deploys the guard is false, so the command is just npm run build, unchanged.
dev and main are migrated the existing way, via .github/workflows/migrate-db.yml on push — that workflow is untouched by the above.
.github/workflows/neon-branch-check.yml checks the live branch count on every PR push and fails when the project is at its cap, so a quota-blocked preview is distinguishable from a broken build without opening the Neon console; the count and full branch inventory are in the run log. It needs two Actions secrets — NEON_API_KEY and NEON_PROJECT_ID, the same values the app already uses at runtime (see .env.example) — and an optional NEON_BRANCH_LIMIT repository variable, which defaults to 10. That limit is configuration rather than something the workflow discovers, because Neon's API exposes no quota endpoint. Never add this check to the required status checks: it goes red at capacity by design, and making it required would block merges on top of Vercel instead of merely reporting.
Preview branches are a project-wide, finite pool: Neon allows 10 branches and 2 are permanently held (dev, production), leaving ~8 usable for previews. Every open PR with a preview deployment consumes one, and merging or closing that PR frees it automatically — there is no manual cleanup step. When the pool is exhausted the integration cannot attach a branch, so DATABASE_URL_UNPOOLED is never injected and the preview build fails on the Prisma datasource — the branch-budget check above is what identifies that as a quota problem. To reclaim capacity, merge or close an open PR; the next push to a blocked branch redeploys into the freed slot.
app/
(main)/ # Authenticated app shell
(auth)/ # Routes requiring login (applications, positions, users, etc.)
profile/ # User profile page
(public)/ # Unauthenticated public routes (position listings)
(app)/ # App-level layout wrappers
(legal)/ # Privacy / terms pages
login/ # Login and auth bypass (dev only)
api/auth/ # Auth callback route (the only API route)
layout.tsx # Root layout
globals.css # Tailwind theme tokens
components/
ui/ # shadcn/ui primitives (Button, Dialog, etc.)
features/ # Domain-specific components (tables, forms, dashboards)
layouts/ # Sidebar, nav, page headers
providers/ # React context providers (auth, query, theme)
prisma/
schema.prisma # Database schema and enums
actions/ # Server Actions — one file per domain
data/ # Server-side data-fetching queries — one file per domain
migrations/ # Auto-generated migration SQL
seed.ts # Demo data seed script
lib/
types.ts # Shared TypeScript types
constants.ts # Shared constants
utils.ts # Utility helpers
auth/ # Auth helpers (server.ts, client.ts)
prisma.ts # Prisma client singleton
middleware.ts # Route auth middleware
XXX-ticket-name-in-kebab-case
where XXX is the GitHub issue number.
Subject line: #XXX imperative lowercase summary (under 80 characters, no trailing period).
Example: #203 add readme for contributors
All three must pass before pushing:
npm run prettier:check
npm run eslint:check
npm run tsc:checkFix formatting with npm run prettier:fix; fix lint issues in code (never with eslint-disable comments).
Issues are tracked in GitHub Issues. Assign yourself before starting: gh issue edit XXX --add-assignee "@me".
| Document | Contents |
|---|---|
CLAUDE.md |
AI-agent conventions, architecture rules, commit/PR format |
.claude/docs/ENGINEERING.md |
Full quality bar: architecture, security, UX states, a11y, performance |
.claude/docs/DESIGN.md |
Design system: tokens, type scale, component conventions |
.claude/docs/PIPELINE.md |
Automated agent pipeline and GitHub label state machine |
.claude/docs/nextjs-notes.md |
Current Next.js 16 App Router behavior — trust over training data |