Skip to content

fringe4life/firstroad

Repository files navigation

First Ticket - A Collaborative Ticket Management Platform

Next.js React TypeScript Prisma Better Auth TailwindCSS Biome nuqs Valibot Elysia Inngest Resend React Email Bun Ultracite

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.

πŸš€ 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 and Bun.s3 works)
  • πŸ’¬ 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 createSlug in @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

πŸ› οΈ Tech Stack

  • 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

⚑ Next.js 16 Modern Features

This project leverages cutting-edge Next.js 16 features for optimal performance and developer experience:

Core Features

  • 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 information
    • viewTransition: Smooth page transitions
    • mcpServer: Model Context Protocol server support
    • typedEnv: Type-safe environment variables

Cache Components & PPR

  • "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

Performance Optimizations

  • 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

βš›οΈ React 19 Modern Patterns

This project leverages cutting-edge React 19.2 features for optimal performance and developer experience:

Activity Component

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

useEffectEvent Hook

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 dependencies

Benefits:

  • Fewer effect re-runs and re-renders
  • Always accesses latest callback values
  • Prevents event listener re-attachment
  • Cleaner dependency arrays

Render Props Pattern

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

startTransition

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

πŸ“‹ Prerequisites

  • Node.js 18+ or Bun
  • PostgreSQL database
  • Git

πŸš€ Getting Started

1. Clone the repository

git clone <repository-url>
cd firstroad

2. Install dependencies

# Using Bun (recommended)
bun install

# Or using npm
npm install

3. Set up environment variables

Copy the web app example env and configure (from repo root):

cp apps/web/env.example apps/web/.env.local

Update 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.

4. Set up the database

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 inngest

Set in apps/web/.env.local:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/firstroad"
INNGEST_DEV=1

This 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.local

5. Start the development server

From repo root (runs Next.js in apps/web via Turborepo):

bun run dev

Open http://localhost:3000 with your browser to see the application.

πŸ“ Project Structure

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

πŸ” Authentication

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

Authentication Flow

  1. Registration: Users sign up with email/password
  2. Email Verification: Verification email sent automatically
  3. Welcome Email: Delayed welcome email sent 2 minutes after signup
  4. Login: Users sign in with verified credentials, OTP, or GitHub OAuth
  5. Social Login: GitHub OAuth integration with automatic redirect to tickets page
  6. OTP Login: Alternative login method using one-time passwords
  7. Password Reset: Users can request password reset via email
  8. Session Management: Secure sessions with cookie-based caching

OTP Authentication Routes

  • 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, and OtpVerifyFormWithConnection (shared server form with redirect) for consistent UX
  • Skeleton Fallback: OtpVerifyFormSkeleton replaces spinner on verify pages
  • InputOTP Component: Enhanced OTP input with shadcn/ui
  • Suspense Patterns: Proper suspension with CardCompact for optimal caching
  • Toast Notifications: Success feedback for OTP sent

Redirect Handling

  • Framework redirects (e.g., redirect() from next/navigation) are preserved by rethrowing redirect errors.
  • Helper: unstable_rethrow rethrows Next.js framework errors.
  • Example usage: Sign-up action rethrows redirect errors to avoid surfacing NEXT_REDIRECT in UI and properly navigate to /.

πŸ”„ Dynamic Rendering & Session Management

  • Dynamic Rendering: Use of connection() from next/server opts routes/components into dynamic rendering
  • User Management: Centralized getUser() in src/features/auth/queries/get-user.ts with defensive session expiration checking
  • HasAuthSuspense Pattern: Suspense-wrapped session injection for auth-dependent components
  • Background Jobs: Inngest handles async operations like password reset emails

HasAuthSuspense Pattern

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

🎫 Ticket System

Features

  • 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)

Sample Data

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

πŸ’¬ Comment System

Features

  • 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 useOptimistic hook
  • State Management: Context store for comments and pagination metadata
  • Real-time Updates: Comments update immediately after actions with server reconciliation

🎨 UI Components

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)

πŸš€ Available Scripts

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 --prod

πŸ”§ Configuration

Build (Next.js + Bun)

Production 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).

Tailwind CSS

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

Database

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

Authentication & Background Jobs

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.window is 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-adapter and related entry points are aliased to a local shim in next.config.ts so Turbopack/Next never load node:sqlite or 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

Email Templates (Resend)

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 reset
  • email-verification - Email verification links
  • password-reset-email - Password reset links
  • password-changed-email - Password change confirmation
  • welcome-email - Welcome message for new users
  • organization-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 address
    • OTP - One-time password code
    • TYPE - Subject line text (computed from OTP type)
  • email-verification:

    • TO_NAME - Recipient name or email address
    • URL - Email verification link
  • password-reset-email:

    • TO_NAME - Recipient name or email address
    • URL - Password reset link
  • password-changed-email:

    • TO_NAME - Recipient name or email address
    • APP_URL - Application base URL
  • welcome-email:

    • TO_NAME - Recipient name or email address
    • APP_URL - Application base URL
  • organization-invitation:

    • TO_NAME - Recipient name or email address
    • ORGANIZATION_NAME - Name of the organization
    • INVITER_NAME - Name of the person who sent the invitation
    • ROLE - 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.

API Routes (Elysia)

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.ts with /api prefix
  • Plugin Pattern: Inngest handler implemented as Elysia plugin in inngest-plugin.ts
  • OpenAPI Support: Automatic API documentation with @elysiajs/openapi 1.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/cors 1.4 for cross-origin requests (applied before auth routes)
  • Better Auth Integration: Auth routes mounted at /auth via auth.handler (CORS-enabled)
  • Inngest Webhooks: Background job webhooks handled at /api/inngest via 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.

Type Safety

  • 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 object
    • Maybe<User>: Session or null for DAL functions
    • ClientSession: 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 createTypedLink for search parameters
  • Slug-based routing with automatic generation and validation

Path Management

Centralized type-safe route definitions in src/path.ts:

  • Static routes with Route type
  • Dynamic routes with as Route assertions
  • Slug-based ticket routes (ticketPath(slug), ticketEditPath(slug))
  • Consistent path usage across the application

Cache Management

Centralized cache tag system for consistent cache invalidation:

  • src/utils/cache-tags.ts: Centralized cache tag functions (similar to path.ts)
    • ticketsCache(), ticketCache(slug), commentsCache(), commentsForTicketCache(ticketSlug), commentCache(commentId), attachmentCache(), attachmentsForTicketCache(ticketId)
  • src/utils/invalidate-cache.ts: High-level invalidation functions
    • invalidateTicketAndList(slug), invalidateCommentsForTicket(ticketSlug), invalidateAttachmentsForTicket(ticketId), invalidateTicketAndAttachments(slug, ticketId), etc.
  • Ensures consistency between cacheTag() and updateTag() calls
  • All ticket-related cache operations use slugs (not IDs) for consistency
  • Single source of truth for cache tag strings

πŸš€ Deployment

Vercel (Recommended)

  1. Connect your GitHub repository to Vercel
  2. Configure environment variables in Vercel dashboard
  3. Deploy automatically on push to main branch

Docker

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-web

The 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).

Other Platforms

The application can be deployed to any platform that supports Next.js:

  • Netlify
  • Railway
  • DigitalOcean App Platform
  • AWS Amplify

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

About

Full-stack collaborative ticket management platform built with Next.js 16, React 19, TypeScript. Features Better Auth, organizations, tickets, comments, infinite pagination, dark mode.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages