Skip to content

Architecture

Know where everything lives. This overview shows how Quackback's layers fit together: routes, server functions, services, and database. Navigate confidently and make changes in the right places.

High-Level Architecture

┌─────────────────────────────────────────────────────────┐
│                     Browser/Client                       │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│  │  Public      │  │  Admin       │  │  Auth        │   │
│  │  Portal      │  │  Dashboard   │  │  Pages       │   │
│  └──────────────┘  └──────────────┘  └──────────────┘   │
└────────────────────────────┬────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────┐
│                  TanStack Start Server                   │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│  │  Routes      │  │  Server      │  │  API         │   │
│  │  (SSR)       │  │  Functions   │  │  Routes      │   │
│  └──────────────┘  └──────────────┘  └──────────────┘   │
│                             │                            │
│  ┌──────────────┐  ┌────────┴───────┐  ┌─────────────┐  │
│  │  Services    │  │  Auth          │  │  Events     │  │
│  │  Layer       │  │  (Better Auth) │  │  System     │  │
│  └──────────────┘  └────────────────┘  └─────────────┘  │
└────────────────────────────┬────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────┐
│                     PostgreSQL                           │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│  │  Drizzle ORM │  │  pgvector    │  │  pg_cron     │   │
│  └──────────────┘  └──────────────┘  └──────────────┘   │
└─────────────────────────────────────────────────────────┘

Tech Stack

LayerTechnologyPurpose
FrameworkTanStack StartFull-stack React framework
RoutingTanStack RouterFile-based routing, type-safe
DatabasePostgreSQL 18+Primary data store
ORMDrizzleType-safe queries
AuthBetter AuthAuthentication library
StylingTailwind CSS v4Utility-first CSS
Componentsshadcn/uiUI component library
ValidationZodRuntime type validation
StateTanStack QueryServer state management
RuntimeBunJavaScript runtime

Directory Structure

apps/web/

The main application:

apps/web/src/
├── routes/                    # File-based routing
│   ├── __root.tsx             # Root layout
│   ├── _portal/               # Public portal routes
│   ├── admin/                 # Admin dashboard
│   ├── auth.*/                # Portal auth
│   ├── admin.login/           # Team auth
│   ├── onboarding/            # Setup wizard
│   └── api/                   # API routes
│
├── components/                # React components
│   ├── admin/                 # Admin-specific
│   ├── public/                # Portal components
│   ├── settings/              # Settings UI
│   └── ui/                    # shadcn/ui primitives
│
├── lib/                       # Business logic
│   ├── server/                # Server-side code
│   │   ├── functions/         # RPC endpoints (server functions)
│   │   ├── domains/           # Service layers by feature
│   │   │   ├── posts/         # Post service
│   │   │   ├── boards/        # Board service
│   │   │   ├── comments/      # Comment service
│   │   │   ├── statuses/      # Status service
│   │   │   ├── tags/          # Tag service
│   │   │   ├── members/       # Member service
│   │   │   ├── users/         # User service
│   │   │   ├── notifications/ # Notification service
│   │   │   ├── subscriptions/ # Subscription service
│   │   │   ├── ai/            # AI integrations
│   │   │   ├── sentiment/     # Sentiment analysis
│   │   │   ├── embeddings/    # Vector embeddings
│   │   │   ├── summary/       # AI post summaries
│   │   │   ├── merge-suggestions/ # AI duplicate detection
│   │   │   ├── changelog/     # Changelog entries
│   │   │   ├── roadmaps/      # Roadmap management
│   │   │   ├── api-keys/      # API key management
│   │   │   └── webhooks/      # Webhook management
│   │   ├── auth/              # Auth configuration
│   │   ├── events/            # Event dispatch & handlers
│   │   ├── integrations/      # Integration handlers
│   │   ├── storage/           # File storage
│   │   └── db.ts              # Database proxy (lazy singleton)
│   │
│   ├── client/                # Client-side code
│   │   ├── hooks/             # React hooks
│   │   ├── queries/           # TanStack Query factories
│   │   ├── mutations/         # TanStack mutation hooks
│   │   ├── stores/            # Zustand stores
│   │   └── query/             # Query client setup
│   │
│   └── shared/                # Utilities & errors
│
└── router.tsx                 # Router configuration

packages/

Shared code:

packages/
├── db/                        # Database layer
│   ├── src/
│   │   ├── schema/            # Drizzle table definitions
│   │   ├── client.ts          # Connection factory
│   │   ├── seed.ts            # Demo data seeding
│   │   └── migrate.ts         # Migration runner
│   └── drizzle/               # Migration SQL files
│
├── ids/                       # TypeID system
│   └── src/
│       ├── index.ts           # ID generation & exports
│       ├── core.ts            # Core ID functions
│       ├── prefixes.ts        # Entity type prefixes
│       ├── types.ts           # Branded type definitions
│       ├── zod.ts             # Validation helpers
│       └── drizzle.ts         # DB column types
│
├── email/                     # Email service
│   └── src/
│       ├── index.ts           # Send functions
│       └── templates/         # React Email templates
│
├── core/                      # Shared core utilities
│
└── integrations/              # Integration connectors

Key Patterns

Server Functions

Type-safe RPC between client and server:

// lib/server/functions/posts.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
 
export const createPostFn = createServerFn({ method: 'POST' })
  .inputValidator(z.object({
    title: z.string().min(1),
    content: z.string(),
    boardId: boardIdSchema,
  }))
  .handler(async ({ data }) => {
    const auth = await requireAuth()
    return createPost(data, auth.member)
  })

Usage in components:

const mutation = useMutation({
  mutationFn: createPostFn,
})

Service Layer

Business logic separate from transport:

// lib/server/domains/posts/post.service.ts
export async function createPost(
  input: CreatePostInput,
  author: Author
): Promise<PostWithDetails> {
  // Validation
  if (!input.title?.trim()) {
    throw new ValidationError('VALIDATION_ERROR', 'Title required')
  }
 
  // Business logic
  const post = await db.insert(posts).values({
    id: createId('post'),
    ...input,
    authorId: author.id,
  }).returning()
 
  // Side effects
  await dispatchPostCreated(post, author)
 
  return post
}

Database Access

Always use the database proxy from @/lib/server/db for database access to ensure proper connection management.

// ✅ Correct - use the proxy
import { db, posts, eq } from '@/lib/server/db'
 
// ❌ Wrong - bypasses tenant context
import { db } from '@quackback/db'

The proxy handles singleton connection initialization with lazy loading.

TypeIDs

Branded UUIDs for type safety:

import { createId, type PostId, type BoardId } from '@quackback/ids'
 
// Create new ID
const postId: PostId = createId('post')
// => "post_01h455vb4pex5vsknk084sn02q"
 
// Type-safe function parameters
function getPost(id: PostId): Promise<Post> {
  // Can't accidentally pass a BoardId here
}

Event System

Fire-and-forget event dispatch:

// lib/server/events/dispatch.ts
export async function dispatchPostCreated(
  post: Post,
  actor: EventActor
) {
  await processEvent({
    type: 'post.created',
    data: { post },
    actor,
  })
}
 
// lib/server/events/process.ts
export async function processEvent(event: AppEvent) {
  const targets = await getHookTargets(event.type)
 
  await Promise.allSettled(
    targets.map(target => target.handler.run(event, target))
  )
}

Hook Handlers

Extensible integration points:

// lib/server/events/handlers/webhook.ts (or similar handler file)
export const slackHandler: HookHandler = {
  async run(event, target, config) {
    const message = formatSlackMessage(event)
    await slack.chat.postMessage({
      channel: config.channelId,
      ...message,
    })
    return { success: true }
  },
}

Route Groups

Public Portal (_portal/)

User-facing feedback portal:

  • Board listing
  • Post detail with comments
  • Voting interface
  • Public roadmap

Admin Dashboard (admin/)

Team management interface:

  • Feedback inbox
  • Roadmap management
  • Settings configuration

Authentication

Two separate auth flows:

  • auth.* - Portal users (password, email OTP, OAuth, OIDC)
  • admin.login - Team members (password, email OTP, OAuth, OIDC)

Data Flow

Create Post Flow

User clicks "Submit" in portal
        │
        ▼
Component calls createPostFn()
        │
        ▼
Server function validates input with Zod
        │
        ▼
requireAuth() checks session
        │
        ▼
createPost() service function
        │
        ▼
Insert into database via Drizzle
        │
        ▼
dispatchPostCreated() event
        │
        ▼
processEvent() runs hooks (async)
        │
        ├──▶ Slack notification
        ├──▶ Email to subscribers
        └──▶ AI sentiment analysis
        │
        ▼
Return post to client
        │
        ▼
React Query updates cache

Deployment

Quackback runs as a single workspace per deployment:

  • DATABASE_URL environment variable
  • Singleton database connection
  • No tenant resolution needed

Next Steps