Best Stack for an Internal Tool
A self-controlled internal tool stack built for small teams. Full data ownership, role-based access, no third-party auth vendor storing your employees' data.
Quick Verdict
Internal tools need different priorities than public SaaS: data ownership matters, user count is small and known, and you don't need OAuth flows or social login. This stack prioritizes control over speed-to-ship. Expect to spend an extra day on auth compared to Clerk.
If data sensitivity isn't a concern, swap Auth.js for Clerk and Neon for Railway Postgres — the rest is the same.
Best For
- Teams with sensitive employee data that cannot live on a third-party vendor
- Admin dashboards that need real RBAC, audit logs, and persistent server behavior
- Long-running reports and batch jobs (Railway has no timeout limits)
- One platform, one bill — Railway hosts both app and database
Avoid If
- Your data isn't sensitive and Clerk would save you a day of auth work
- You're building a simple read-only dashboard (a no-code tool is faster)
- You need complex real-time features — those are independent of this auth/data choice
Why This Stack Differs From SaaS MVP
The key decision is Auth.js over Clerk. For internal tools handling sensitive business data, storing employee identities on a third-party service introduces vendor risk and compliance concerns. Auth.js keeps sessions and user records in your own Postgres database.
Railway over Vercel: internal tools often have persistent background jobs, scheduled reports, and long operations. Railway's persistent server has no cold starts and no timeouts. Your scheduled report at 3am runs to completion.
What It Optimizes For
- Full data ownership — users, sessions, audit logs all in your Postgres
- No cold starts (Railway persistent server)
- Persistent compute (batch jobs, scheduled reports, long operations)
- Real RBAC without external dependency
What It Sacrifices
- Auth setup speed — Auth.js takes 4–6 hours vs Clerk's 30 minutes
- Zero-config deployment — Railway requires more setup than Vercel
- $0/month infra — Railway starts at $5–20/month from day one
Implementation Order
npx create-next-app@latest --typescript --tailwind --app- Deploy to Railway first — confirm the deployment environment works before building features
- Set up Railway Postgres + Drizzle schema —
users,sessions,rolestables - Configure Auth.js with Credentials provider + email domain allowlist
- Build role-based middleware at the layout level (not per-page)
- Set up
audit_logtable —(id, user_id, action, entity, entity_id, timestamp) - Add shadcn/ui data tables for core data views
- Build CRUD forms with audit logging on every write
Do Now / Do Later
Do now: Auth with RBAC from day one (don't add roles later — design the table flexibly), audit log from day one (retrofitting is painful).
Do later: Advanced permission systems, background job queues, multi-tenant support. Internal tools rarely need these at the start.
What Breaks First
- Auth.js API changes — v5 had breaking changes. Pin your version and test upgrades in a branch.
- RBAC complexity growth — internal tools start simple ("admin" and "viewer") and grow into complex permission trees. Design your
rolestable to be data-driven, not hardcoded enums. - Missing audit log — adding this after the fact requires retrofitting every write operation. Do it from day one.
- Railway cost surprises — no meaningful free tier. Budget $10–20/month from the start.
AI Coding Notes
Auth.js setup is more complex than Clerk, and AI tools generate more errors for it. The most common: generating per-page auth checks instead of centralized middleware. Use auth() in your root layout, not in each page component.
Common AI Mistakes
- Per-page auth checks instead of layout-level middleware — generates duplicated boilerplate
- Missing RBAC in the middleware — AI scaffolds auth without role enforcement
- No audit log on write operations — AI generates CRUD without history
- Using Vercel serverless for batch jobs — Railway persistent server is required for long operations
- Hardcoding roles as TypeScript string literals instead of a database-driven enum
Migration Warning
Moderate. Auth.js sessions and user records are in your Postgres — no vendor migration needed. If you outgrow Railway, your app is a standard Node.js/Next.js project that runs anywhere. The hardest migration is the data itself.
Confidence Score — Why
8/10. Well-proven for internal admin tools. Deducted 2 points: Auth.js requires more upfront work and Rails churn history (v5 broke existing setups). The data ownership trade-off is correct, but it costs real development time.
Starter Config Files
# My Internal Tool Stack
- Framework: **Next.js 15** (App Router, TypeScript)
- Styling: **Tailwind CSS** + shadcn/ui
- Auth: **Auth.js v5** (Credentials provider, sessions in PostgreSQL)
- Database: **PostgreSQL** on Railway (persistent, not serverless)
- ORM: **Drizzle ORM** (pg driver)
- Deployment: **Railway** (persistent server, no cold starts)Unlock full config files
Copy, download as .zip, and see all 5 complete files for this stack.