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
| Layer | Technology | Purpose |
|---|---|---|
| Framework | TanStack Start | Full-stack React framework |
| Routing | TanStack Router | File-based routing, type-safe |
| Database | PostgreSQL 18+ | Primary data store |
| ORM | Drizzle | Type-safe queries |
| Auth | Better Auth | Authentication library |
| Styling | Tailwind CSS v4 | Utility-first CSS |
| Components | shadcn/ui | UI component library |
| Validation | Zod | Runtime type validation |
| State | TanStack Query | Server state management |
| Runtime | Bun | JavaScript 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_URLenvironment variable- Singleton database connection
- No tenant resolution needed
Next Steps
- Server Functions - Deep dive into RPC patterns
- Database - Schema and migrations
- Adding Features - Build new functionality