A full-stack collaborative platform built with Next.js 16, featuring authentication, real-time updates, and a modern UI. Users can create, manage, and track tickets with comments and collaboration features.
- π Authentication: Secure user authentication with Better Auth (email/password + OTP + GitHub OAuth) with email enumeration protection
- π’ Organization Management: Create and manage organizations with membership and invitation systems, role-based access control (owner, admin, member), granular permissions (canDeleteTicket), and admin tabs for managing members and invitations
- π« Ticket Management: Create, edit, and manage tickets with status tracking
- π Ticket Attachments: Owner-only file uploads with Bun S3, image previews before upload, and owner-only delete actions; presigned download URLs for all viewers (Bun runtime; on Vercel use
bunVersion: "1.x"in vercel.json so Server Actions run on Bun andBun.s3works) - π¬ Comments System: Add, edit, and delete comments on tickets with infinite pagination and owner-only attachments
- π Dark Mode: Beautiful light/dark theme with smooth transitions
- π± Responsive Design: Optimized for desktop and mobile devices with PPR navigation and cached components
- β‘ Real-time Updates: Server-side rendering with React Suspense and PPR dynamic holes
- π Search & Filter: Advanced search and filtering capabilities
- π¨ Modern UI: Built with shadcn/ui components and Tailwind CSS
- π Infinite Pagination: Efficient cursor-based pagination for comments
- π Ownership System: Users can only edit their own tickets and comments
- π― Type Safety: Full TypeScript support with typed routes
- π§ Email Features: Password reset, email verification, OTP authentication, and welcome emails with Resend templates
- π Slug Generation: Human-readable URLs via shared
createSlugin@firstroad/utils(packages/utils) - π Parallel Routes: Next.js parallel routes (@auth) for authentication modals with interception routes
- β‘ React Compiler: React 19 compiler for automatic performance optimization
- π¬ Background Jobs: Inngest for async event handling and email processing
- β‘ PPR Navigation: Partial Prerendering with dynamic auth components
- π Session Management: Cookie-based session caching with defensive expiration checks
- π Slug-based Routing: Human-readable URLs using ticket slugs instead of IDs
- π± Responsive Controls: Desktop button groups and mobile dropdowns for ticket filtering
- Framework: Next.js 16.1 (App Router) with Turbopack
- Language: TypeScript 5.9 with strict type checking
- Database: PostgreSQL with Prisma Client 7.4 (relationJoins preview, Neon adapter)
- Authentication: Better Auth 1.5 (beta) with email/password provider and session cookie caching
- Styling: Tailwind CSS v4.2.1 with shadcn/ui components
- Icons: Lucide React
- Forms: React Hook Form with Valibot validation
- Notifications: Sonner toast notifications
- Theme: next-themes for dark/light mode
- URL Search Params: nuqs 2.8 for type-safe URL parameters
- Email: React Email 5.2 with Resend 6.9 for transactional emails
- API Framework: Elysia 1.4 with @elysiajs/cors 1.4 for unified API routes
- Background Jobs: Inngest 3.52.4 for background tasks and event handling
- Package Manager: Bun (recommended)
- Shared Utilities:
@firstroad/utils(packages/utils) for shared helpers (e.g.createSlug) - Linting: Biome 2.4.0 for fast formatting and linting with Ultracite 7.2 rules
- Type Checking: TypeScript native preview for fast checking
- React Compiler: React 19 compiler for performance optimization
This project leverages cutting-edge Next.js 16 features for optimal performance and developer experience:
- Typed Routes: Full type safety for all routes (
typedRoutes: true) - Turbopack: Fast bundling for development and production
- React Compiler: React 19 compiler for automatic performance optimization
- Cache Components: Function-level caching with
cacheComponents: true - Parallel Routes: Authentication modals with interception routes (
@auth) - Interception Routes: Modal overlays with graceful fallback on hard refresh
- Experimental Features:
browserDebugInfoInTerminal: Enhanced debugging informationviewTransition: Smooth page transitionsmcpServer: Model Context Protocol server supporttypedEnv: Type-safe environment variables
- "use cache" Directive: Function-level caching for data queries and static components
- PPR (Partial Prerendering): Static shell with dynamic holes for optimal performance
- Slug-based Routing: Human-readable URLs with automatic slug generation
- Type-safe Search Parameters: nuqs integration for URL parameter management
- Static Shell Prerendering: Header and Sidebar components are prerendered for instant loading
- Dynamic Streaming: Auth-dependent components stream in with Suspense boundaries
- Cache Lifecycle Management: Strategic caching with
cacheLife()for optimal performance - Background Rendering: Lower-priority rendering for hidden content
This project leverages cutting-edge React 19.2 features for optimal performance and developer experience:
Pre-renders hidden content at lower priority for instant transitions:
// Skeletons pre-render in background, ready before loading starts
<Activity mode={isPending ? "visible" : "hidden"}>
<Skeleton />
<Skeleton />
<Skeleton />
</Activity>
// Comments component stays mounted when hidden, preserving state
<Activity mode={isDetail ? "visible" : "hidden"}>
<Comments {...commentProps} />
</Activity>Benefits:
- Instant visual feedback on state changes
- Preserves component state when hidden
- Reduces perceived latency
- Lower-priority background rendering
Prevents unnecessary effect re-runs by extracting non-reactive callbacks:
// Callbacks no longer trigger effect re-synchronization
const handleSuccess = useEffectEvent(() => {
onSuccess?.({ state });
});
useEffect(() => {
if (state.status === "SUCCESS") {
handleSuccess();
}
}, [state]); // β
Callbacks not in dependenciesBenefits:
- Fewer effect re-runs and re-renders
- Always accesses latest callback values
- Prevents event listener re-attachment
- Cleaner dependency arrays
Modern alternative to cloneElement for explicit data flow:
// Before: cloneElement (implicit prop injection)
<ConfirmDialog trigger={<Button>Delete</Button>} />
// After: Render props (explicit prop passing)
<ConfirmDialog
trigger={({ isPending, onClick }) => (
<Button onClick={onClick} disabled={isPending}>
Delete
</Button>
)}
/>Benefits:
- Explicit data flow
- Full TypeScript support
- Better component reusability
- React team recommended approach
Non-blocking state updates for smoother UX:
startTransition(() => {
setComments((prev) => [...prev, ...result.list]);
setNextCursor(result.nextCursor);
setHasMore(result.hasMore);
});Benefits:
- UI stays responsive during updates
- Lower priority for non-urgent updates
- Better perceived performance
- Avoids blocking user interactions
- Node.js 18+ or Bun
- PostgreSQL database
- Git
git clone <repository-url>
cd firstroad# Using Bun (recommended)
bun install
# Or using npm
npm installCopy the web app example env and configure (from repo root):
cp apps/web/env.example apps/web/.env.localUpdate apps/web/.env.local with your configuration:
# Database (for Docker Postgres: postgresql://postgres:postgres@localhost:5432/firstroad)
DATABASE_URL="postgresql://username:password@localhost:5432/your_database"
DIRECT_URL="postgresql://username:password@localhost:5432/your_database"
# Inngest (set INNGEST_DEV=1 for local dev with docker-compose inngest service)
# INNGEST_DEV=1
# Auth (Better Auth; validated as BETTER_AUTH_SECRET in src/lib/env.ts)
BETTER_AUTH_SECRET="your-secret-key-here"
# Public app URL used for emails and redirects
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# Email (Resend)
# Docs: https://resend.com/
RESEND_API_KEY="your-resend-api-key"
# Resend Email Configuration
# NEXT_PUBLIC_RESEND_FROM should be an email address, not an HTTP URL
# Format: "Your App Name <your-email@domain.com>" or just "your-email@domain.com"
NEXT_PUBLIC_RESEND_FROM="Your App <onboarding@resend.dev>"
# Optional: Resend Full Access key for template scripts (resend:list, resend:download)
# RESEND_FULL_ACCESS="re_..."
# Social Authentication (GitHub)
# Docs: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
# S3 (Bun runtime only; used for ticket attachments)
# Docs: https://bun.sh/docs/runtime/s3
# Bun.s3 works in Server Actions with Bun runtime (bun run dev/start). On Vercel, set "bunVersion": "1.x" in vercel.json so the app runs on Bun and Bun.s3 is available.
S3_ACCESS_KEY_ID="your-s3-access-key-id"
S3_SECRET_ACCESS_KEY="your-s3-secret-access-key"
S3_REGION="us-east-1"
S3_BUCKET="your-bucket-name"
# Optional: for non-AWS S3-compatible services (e.g. MinIO, R2)
# S3_ENDPOINT="https://s3.us-east-1.amazonaws.com"Note: DIRECT_URL is optional and only needed for connection pooling scenarios. The application works with just DATABASE_URL configured. DIRECT_URL is not validated in the environment schema.
Option A: Docker Postgres (recommended for local dev)
Start Postgres and Inngest Dev Server in Docker:
docker compose up -d postgres inngest
# Or with env file to avoid build-arg warnings: docker compose --env-file apps/web/.env up -d postgres inngestSet in apps/web/.env.local:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/firstroad"
INNGEST_DEV=1This keeps all dev DB traffic local and avoids Neon free-tier limits. Inngest Dev Server UI: http://localhost:8288.
Option B: External Postgres (e.g. Neon)
Use your Neon or other Postgres URL for DATABASE_URL. Omit INNGEST_DEV if using Inngest Cloud; for local Inngest, run bun run inngest from root and set INNGEST_DEV=1.
Prisma lives in packages/database. Use .env for production/Neon; use .env.local for local Docker Postgres (copy from packages/database/env.example).
From repo root:
# Generate Prisma client (runs automatically after bun install via postinstall)
bunx turbo run db:generate --filter=@firstroad/db
# Run migrations (from packages/database)
cd packages/database && bun run db:migrate # uses .env (prod)
cd packages/database && bun run db:migrate:local # uses .env.local (local Docker)
# Or push schema for dev
cd packages/database && bun run db:push # uses .env
cd packages/database && bun run db:push:local # uses .env.local
# Seed (from packages/database; seed script in prisma.config.ts)
cd packages/database && bun run db:seed # uses .env
cd packages/database && bun run db:seed:local # uses .env.localFrom repo root (runs Next.js in apps/web via Turborepo):
bun run devOpen http://localhost:3000 with your browser to see the application.
Turborepo monorepo: root workspace with apps/* and packages/*.
firstroad/
βββ .dockerignore
βββ docker-compose.yml
βββ apps/
β βββ web/ # Next.js app (main app)
β β βββ src/
β β β βββ app/ # App Router: (auth), (password), @auth, api, onboarding, layout, page
β β β βββ components/ # Shared UI, form, skeletons, theme, ui
β β β βββ features/ # attachments, auth, comment, invitations, memberships, navigation, organisation, pagination, password, ticket
β β β βββ lib/ # auth, env, inngest, email, app
β β β βββ hooks/ # Shared client hooks
β β β βββ utils/ # cache-tags, invalidate-cache, slug, to-action-state, etc.
β β β βββ path.ts
β β β βββ proxy.ts
β β βββ Dockerfile
β β βββ env.example
β β βββ next.config.ts
β β βββ vercel.json
β βββ inngest/ # Inngest dev tooling
βββ packages/
β βββ database/ # Prisma (schema, migrations, models, seed)
β β βββ env.example # Copy to .env or .env.local
β β βββ prisma/
β β βββ prisma.config.ts
β β βββ src/ # client, client-types, index
β βββ emails/ # React Email templates
β βββ utils/ # Shared utilities (e.g. createSlug for slugs)
β βββ src/ # index, slug
βββ package.json # Workspaces, turbo scripts
βββ turbo.json
βββ biome.jsonc
The application uses Better Auth with multiple authentication methods:
- Sign Up: Create new accounts with email and password
- Sign In: Secure login with credential validation or OTP
- Social Login: GitHub OAuth authentication (working) with automatic redirect to tickets page
- OTP Authentication: One-time password authentication via email
- Sign-in OTP: Alternative login method using one-time passwords
- Email Verification OTP: Verify email addresses with OTP codes
- Password Reset: Built-in password reset functionality
- Email Verification: Automatic email verification on signup
- Welcome Emails: Delayed welcome emails sent 2 minutes after signup
- Protected Routes: Automatic redirection for unauthenticated users
- User Sessions: Secure session management
- Registration: Users sign up with email/password
- Email Verification: Verification email sent automatically
- Welcome Email: Delayed welcome email sent 2 minutes after signup
- Login: Users sign in with verified credentials, OTP, or GitHub OAuth
- Social Login: GitHub OAuth integration with automatic redirect to tickets page
- OTP Login: Alternative login method using one-time passwords
- Password Reset: Users can request password reset via email
- Session Management: Secure sessions with cookie-based caching
- Sign-in OTP:
/sign-in/otp/sendβ/sign-in/otp/verify - Email Verification OTP:
/verify-email/otp/sendβ/verify-email/otp/verify - Dedicated Server Actions: Purpose-specific actions for each OTP flow
- Reusable Components:
OTPSendForm,OTPVerifyForm, andOtpVerifyFormWithConnection(shared server form with redirect) for consistent UX - Skeleton Fallback:
OtpVerifyFormSkeletonreplaces spinner on verify pages - InputOTP Component: Enhanced OTP input with shadcn/ui
- Suspense Patterns: Proper suspension with
CardCompactfor optimal caching - Toast Notifications: Success feedback for OTP sent
- Framework redirects (e.g.,
redirect()fromnext/navigation) are preserved by rethrowing redirect errors. - Helper:
unstable_rethrowrethrows Next.js framework errors. - Example usage: Sign-up action rethrows redirect errors to avoid surfacing
NEXT_REDIRECTin UI and properly navigate to/.
- Dynamic Rendering: Use of
connection()fromnext/serveropts routes/components into dynamic rendering - User Management: Centralized
getUser()insrc/features/auth/queries/get-user.tswith defensive session expiration checking - HasAuthSuspense Pattern: Suspense-wrapped session injection for auth-dependent components
- Background Jobs: Inngest handles async operations like password reset emails
Components use the HasAuthSuspense pattern for session-dependent rendering:
// In page components (e.g., ticket detail page)
<TicketItem
comments={
<HasAuthSuspense fallback={<div>Loading Comments...</div>}>
{(user) => (
<Comments
deleteCommentAction={deleteComment}
list={list}
loadMoreAction={getCommentsByTicketSlug}
metadata={metadata}
ticketId={ticket.id}
ticketSlug={ticket.slug}
upsertCommentAction={upsertComment}
userId={user?.id}
userName={user?.name}
/>
)}
</HasAuthSuspense>
}
isDetail={true}
ticket={ticket}
/>This pattern enables:
- Suspense-based loading states for auth-dependent content
- Function-level caching with "use cache" for static components
- Proper authorization checks via session context
- Type-safe session handling with
MaybeServerSession
- Create Tickets: Users can create tickets with title, description, and deadline
- Attachments: Ticket owners can upload files (Bun S3); presigned download URLs for all viewers; owner-gated upload form with HasAuthSuspense
- Status Management: Track ticket status (Open, In Progress, Done)
- Ownership: Users can only edit their own tickets
- Search & Filter: Find tickets by title, description, or status
- Deadline Tracking: Set and manage ticket deadlines
- Slug-based URLs: Human-readable URLs using ticket slugs (e.g.,
/this-ticket-title) - Unified Ticket Pages: Ticket creation form and list displayed on the same page
- Responsive Controls: Desktop button groups and mobile dropdowns for filtering
- Batch Access Queries: Ownership and permissions fetched in 1 batch query for list pages (vs N individual queries)
The database is seeded with sample tickets and comments for existing users:
- Seeding: Only creates tickets and comments, preserves existing users
- User Creation: Users must be created through the application's sign-up flow
- Add Comments: Users can add comments to tickets with optimistic UI updates
- Edit Comments: Comment owners can edit their comments with optimistic UI updates
- Delete Comments: Comment owners can delete their comments with optimistic UI updates
- Comment Attachments: Owner-only file uploads on comments (Bun S3, same as ticket attachments); optimistic placeholders show real file names while uploading (valibot-free client parsing via
getFilesFromFormData) - Infinite Pagination: Efficient cursor-based pagination for large comment lists
- Optimistic Updates: Instant UI feedback using React 19's
useOptimistichook - State Management: Context store for comments and pagination metadata
- Real-time Updates: Comments update immediately after actions with server reconciliation
Built with shadcn/ui and Tailwind CSS:
- Responsive Design: Works on all device sizes
- Dark Mode: Toggle between light and dark themes
- Accessible: WCAG compliant components
- Customizable: Easy to modify and extend
- Loading States: Skeleton components for better UX
- Card Components: Consistent card layouts for auth pages
- Table Components: shadcn Table component for data display (used in organisation list)
All commands from repo root. Turborepo runs tasks in the right workspace; Next.js app is in apps/web.
# Development
bun run dev # Start dev server (Turbopack, apps/web)
bun run dev:inspect # Start dev with inspector
bun run build # Build (turbo; excludes emails package)
bun run start # Start production server
bun run type # TypeScript type check (tsgo)
bun run typegen # Next.js type definitions (apps/web)
bun run next:upgrade # Upgrade Next.js
bun run next:analyze # Bundle analysis
bun run postinstall # Generate Prisma client (turbo postinstall)
# Email (packages/emails)
bun run dev:email # React Email preview
bun run build:email # Build email templates
bun run export:email # Export to HTML
bun run resend:list # List Resend templates (RESEND_FULL_ACCESS)
bun run resend:download # Download templates to emails/downloaded/
# Database (packages/database via turbo)
bunx turbo run db:generate --filter=@firstroad/db # Generate Prisma client
cd packages/database && bun run db:migrate:local # Migrate local Docker DB
cd packages/database && bun run db:push:local # Push schema to local
cd packages/database && bun run db:seed:local # Seed local DB
bun run reset:tickets # Reset ticket/comment data (preserves users)
bun run clear:non-auth # Clear non-auth tables
bun run clear:attachments # Clear attachment records and S3 objects
bun run seed:members # Seed org members
# Inngest
bun run inngest # Inngest dev server (local)
# Code quality (Ultracite + Biome)
bun run check # Ultracite check
bun run fix # Ultracite fix (format + lint)
bun run doctor # Ultracite doctor
# Deployment
bun run deploy:prod # cd apps/web && vercel deploy --prodProduction build supports Turbopack (bun run next build --turbopack). To avoid Better Auth Kysely adapter chunk errors with Bun + Next 16, next.config.ts uses serverExternalPackages for node:sqlite, @better-auth/kysely-adapter, and related Better Auth adapter paths (see better-auth#6781).
The project uses Tailwind CSS v4 with custom configuration for dark mode, theme variables, and custom variants:
- CSS Variables: Dynamic layout calculations with CSS custom properties
- Layout Shift Prevention: CSS-driven height consistency and responsive design
PostgreSQL with Prisma Client 7.4 using:
- relationJoins preview feature for optimized queries
- Client-side engine for edge compatibility
- Neon serverless adapter for efficient connections
- Custom output path:
generated/prisma/(in packages/database)
Database Models:
- User: Better Auth user model with direct relations to tickets and comments
- Account: Better Auth account model
- Session: Better Auth session model
- Verification: Better Auth verification tokens
- Organization: Organization management
- Member: Organization membership with role-based permissions (owner, admin, member) and granular permissions (canDeleteTicket)
- Invitation: Organization invitations with role assignment
- Ticket: Ticket management with unique slug field (direct relation to User)
- TicketAttachment: Ticket file attachments; S3 key convention
attachments/{ticketId}/{attachmentId}/{fileName} - Comment: Comment system (direct relation to User)
- CommentAttachment: Comment file attachments
Better Auth configured with:
- Email/password authentication
- Password reset functionality with Resend templates via Inngest events
- Email verification
- Rate limiting for production security (Better Auth v1.5)
- v1.5 improves the rate limiter: rejected requests are no longer counted, the memory backend has expired-entry cleanup, and built-in defaults are stricter (e.g. sign-in/sign-up, password reset/OTP).
- Note: Rate limiting uses in-memory storage, which does not persist across serverless function invocations on Vercel. For a portfolio project this is acceptable. A proper fix would use database or secondary storage for rate limits when supported. If using secondary storage (e.g. Redis), GitHub issue #5452 is closed unresolved: custom
rateLimit.windowis not passed as TTL for some paths (hardcoded 10s), so the limitation still applies.
- Prisma Client with Neon driver adapter; Better Auth uses
@better-auth/prisma-adapter. - Session cookie caching (5-minute cache duration)
- Session expiration (7 days) and update age (1 day)
- Kysely adapter shim (required):
@better-auth/kysely-adapterand related entry points are aliased to a local shim innext.config.tsso Turbopack/Next never loadnode:sqliteor the real Kysely adapter. Bun 1.3.10 and Better Auth v1.5 have not resolved this incompatibility; the shim is still required. Builds succeed locally (Bun 1.3.10) and deploy to Vercel (Bun runtime 1.3.6) with the shim enabled.
Inngest provides background job processing for:
- Password reset emails
- Email verification
- OTP authentication emails
- Welcome emails (2-minute delay)
- Async event handling
The application uses Resend 6.9 for transactional emails with published templates. All email sending functions use Resend's template API instead of inline React Email components.
Template IDs:
email-otp-verification- OTP codes for sign-in, email verification, and password resetemail-verification- Email verification linkspassword-reset-email- Password reset linkspassword-changed-email- Password change confirmationwelcome-email- Welcome message for new usersorganization-invitation- Organization membership invitations
Template Variables:
All templates use UPPERCASE_SNAKE_CASE variable names (Resend requirement). Variables are passed via the template.variables object in resend.emails.send():
-
email-otp-verification:TO_NAME- Recipient name or email addressOTP- One-time password codeTYPE- Subject line text (computed from OTP type)
-
email-verification:TO_NAME- Recipient name or email addressURL- Email verification link
-
password-reset-email:TO_NAME- Recipient name or email addressURL- Password reset link
-
password-changed-email:TO_NAME- Recipient name or email addressAPP_URL- Application base URL
-
welcome-email:TO_NAME- Recipient name or email addressAPP_URL- Application base URL
-
organization-invitation:TO_NAME- Recipient name or email addressORGANIZATION_NAME- Name of the organizationINVITER_NAME- Name of the person who sent the invitationROLE- Assigned role (member or admin)INVITE_URL- Invitation acceptance link
Usage in Templates:
Variables are accessed in Resend template HTML using triple curly braces: {{{VARIABLE_NAME}}}. For example:
<h1>Hello {{{TO_NAME}}}</h1>
<a href="{{{URL}}}">Verify Email</a>API Key Requirements:
Resend templates require an API key with full_access permissions (not just sending_access) to create and manage templates. The RESEND_API_KEY environment variable must be configured with a full-access key.
The application uses Elysia 1.4 as a unified API framework for handling all API routes through a single catch-all handler (src/app/api/[[...slugs]]/route.ts).
Architecture:
- Centralized App Instance: Elysia app created in
src/lib/app.tswith/apiprefix - Plugin Pattern: Inngest handler implemented as Elysia plugin in
inngest-plugin.ts - OpenAPI Support: Automatic API documentation with
@elysiajs/openapi1.4 (currently disabled due to specPath maximum call stack size exceeded error)
Features:
- Unified API Handler: Single Elysia instance handles all API routes
- CORS Support: Configured with
@elysiajs/cors1.4 for cross-origin requests (applied before auth routes) - Better Auth Integration: Auth routes mounted at
/authviaauth.handler(CORS-enabled) - Inngest Webhooks: Background job webhooks handled at
/api/inngestvia Elysia plugin - Next.js Route Handlers: Exports GET, POST, PUT, DELETE, OPTIONS handlers for Next.js App Router
Route Structure:
/api/auth/*- Better Auth authentication endpoints/api/tickets- GET endpoint for listing tickets with pagination, search, and sorting/api/tickets/:slug- GET endpoint for retrieving a single ticket by slug/api/inngest- Inngest webhook endpoint for background jobs
Configuration:
- CORS origin:
process.env.NEXT_PUBLIC_APP_URL - Methods: GET, POST, PUT, DELETE, OPTIONS
- Credentials: enabled
- Allowed headers: Content-Type, Authorization
Benefits:
- Type-safe API routes with Elysia's TypeScript support
- Unified middleware and CORS configuration
- Better Auth and Inngest integration in a single handler
- Plugin-based architecture for modular route management
- Automatic OpenAPI documentation generation (currently disabled - see known issues)
Known Issues:
- OpenAPI plugin causes "Maximum call stack size exceeded" error at specPath, likely due to circular references when introspecting mounted routes (Better Auth handler). OpenAPI generation is currently disabled until this issue is resolved.
- Full TypeScript support with strict configuration
- Typed routes with Next.js 16 (
typedRoutes: true) - Type-safe URL search parameters with nuqs 2.8
- Centralized auth types in
src/features/auth/types.ts:ServerSession: Full session with user objectMaybe<User>: Session or null for DAL functionsClientSession: Client-side session type
- Discriminated union types for compile-time prop validation (e.g.,
TicketItemProps) - HasAuthSuspense pattern with session injection for auth-dependent components
- Shared utilities in
src/utils/for better organization - Type-safe link generation with
createTypedLinkfor search parameters - Slug-based routing with automatic generation and validation
Centralized type-safe route definitions in src/path.ts:
- Static routes with
Routetype - Dynamic routes with
as Routeassertions - Slug-based ticket routes (
ticketPath(slug),ticketEditPath(slug)) - Consistent path usage across the application
Centralized cache tag system for consistent cache invalidation:
src/utils/cache-tags.ts: Centralized cache tag functions (similar topath.ts)ticketsCache(),ticketCache(slug),commentsCache(),commentsForTicketCache(ticketSlug),commentCache(commentId),attachmentCache(),attachmentsForTicketCache(ticketId)
src/utils/invalidate-cache.ts: High-level invalidation functionsinvalidateTicketAndList(slug),invalidateCommentsForTicket(ticketSlug),invalidateAttachmentsForTicket(ticketId),invalidateTicketAndAttachments(slug, ticketId), etc.
- Ensures consistency between
cacheTag()andupdateTag()calls - All ticket-related cache operations use slugs (not IDs) for consistency
- Single source of truth for cache tag strings
- Connect your GitHub repository to Vercel
- Configure environment variables in Vercel dashboard
- Deploy automatically on push to main branch
Build the production image (requires Neon DATABASE_URL for generateStaticParams at build time):
docker build -f apps/web/Dockerfile \
--build-arg DATABASE_URL="$NEON_DATABASE_URL" \
-t firstroad-web .Run the container (pass all required env vars: DATABASE_URL, BETTER_AUTH_SECRET, NEXT_PUBLIC_APP_URL, RESEND_*, GITHUB_*, S3_*, INNGEST_SIGNING_KEY, INNGEST_EVENT_KEY):
docker run -p 3000:3000 \
-e DATABASE_URL="$NEON_DATABASE_URL" \
-e BETTER_AUTH_SECRET="..." \
-e NEXT_PUBLIC_APP_URL="https://your-domain.com" \
# ... other env vars
firstroad-webThe app uses Bun runtime (required for Bun.s3 attachments). For production, use Inngest Cloud (set INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY; do not set INNGEST_DEV).
The application can be deployed to any platform that supports Next.js:
- Netlify
- Railway
- DigitalOcean App Platform
- AWS Amplify
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Next.js - React framework
- shadcn/ui - UI components
- Better Auth - Authentication
- Prisma - Database ORM
- Tailwind CSS - CSS framework
- nuqs - Type-safe URL search params
- Biome - Fast formatting and linting
- Ultracite - Biome rules enforcement
- React Compiler - Performance optimization
- React Email - Email templates
- Inngest - Background job processing
- Valibot - Lightweight schema validation
- Elysia - Typesafe, fast API management