Architecture

Deep dive into BrewHoard's project structure, design patterns, and architectural decisions.

Understanding BrewHoard’s architecture helps you navigate the codebase efficiently and make informed decisions when extending functionality.

High-Level Overview

BrewHoard follows a layered architecture with clear separation of concerns:

Loading diagram...

Directory Structure

Text
beer-vault/
├── src/
│   ├── routes/                 # SvelteKit routing
│   │   ├── (app)/              # Authenticated routes (route group)
│   │   │   ├── collection/     # Beer collection pages
│   │   │   ├── marketplace/    # Trading marketplace
│   │   │   ├── scan/           # Beer scanner
│   │   │   ├── settings/       # User settings
│   │   │   └── +layout.svelte  # App shell with nav
│   │   ├── api/                # REST API endpoints
│   │   │   └── v1/             # Versioned API
│   │   ├── docs/               # Documentation (you're here!)
│   │   └── +layout.svelte      # Root layout
│   │
│   ├── lib/                    # Shared code
│   │   ├── auth/               # Authentication logic
│   │   ├── beer/               # Beer-related utilities
│   │   ├── collection/         # Collection management
│   │   ├── marketplace/        # Marketplace logic
│   │   ├── scanner/            # Image recognition
│   │   ├── ratings/            # Review system
│   │   ├── notifications/      # Email/push notifications
│   │   ├── payments/           # Stripe integration
│   │   ├── storage/            # File uploads
│   │   ├── server/             # Server-only code
│   │   │   └── db.js           # Database connection
│   │   ├── utils/              # Shared utilities
│   │   ├── components/         # Shared UI components
│   │   │   └── ui/             # shadcn-svelte components
│   │   └── paraglide/          # Generated i18n code
│   │
│   ├── components/             # Feature-specific components
│   │   ├── auth/               # Login, registration forms
│   │   ├── beer/               # Beer cards, lists
│   │   ├── collection/         # Collection UI
│   │   ├── marketplace/        # Listing forms, cards
│   │   └── ratings/            # Review forms, displays
│   │
│   ├── hooks.js                # Client hooks
│   ├── hooks.server.js         # Server hooks (auth, etc.)
│   ├── app.css                 # Global styles
│   ├── app.html                # HTML template
│   └── app.d.ts                # TypeScript declarations

├── messages/                   # i18n message files
│   ├── en.json                 # English
│   ├── nl.json                 # Dutch
│   └── ...                     # Other languages

├── migrations/                 # Database migrations
│   ├── 00001_initial/
│   ├── 00002_marketplace_fields/
│   └── ...

├── static/                     # Static assets
│   ├── pwa-192x192.png
│   └── ...

└── tests/                      # Test files
    ├── unit/
    └── e2e/

Key Concepts

Route Groups

SvelteKit uses route groups (directories in parentheses) to organize routes without affecting URLs:

Text
routes/
├── (app)/              # Authenticated routes → /collection, /marketplace
│   ├── collection/
│   └── +layout.svelte  # Shared app layout with navigation
├── (marketing)/        # Public routes → /about, /pricing
│   └── +layout.svelte  # Marketing layout
└── +layout.svelte      # Root layout (always applied)

The (app) group shares a layout with navigation, authentication checks, and the app shell.

Server vs Client Code

BrewHoard carefully separates server-only and client-safe code:

JavaScript
// src/lib/server/db.js - SERVER ONLY
// This file can only be imported in +server.js or +page.server.js
import postgres from 'postgres';
export default postgres(process.env.DATABASE_URL);

// src/lib/utils/format.js - SHARED
// Safe to use anywhere
export function formatCurrency(amount, currency = 'USD') {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency
  }).format(amount);
}

SvelteKit enforces this at build time - importing server code in client code causes an error.

Hooks Architecture

Hooks intercept requests at various lifecycle points:

JavaScript
// src/hooks.server.js
import { sequence } from '@sveltejs/kit/hooks';
import { authHook } from '$lib/middleware/auth.js';
import { rateLimitHook } from '$lib/middleware/rate-limit.js';

// Hooks run in sequence for every request
export const handle = sequence(
  authHook,      // 1. Parse session, set locals.user
  rateLimitHook  // 2. Check rate limits
);

The locals object passes data between hooks and route handlers:

JavaScript
// In hooks.server.js
event.locals.user = await validateSession(sessionId);

// In +page.server.js
export function load({ locals }) {
  if (!locals.user) redirect(303, '/login');
  return { user: locals.user };
}

Data Flow

Page Load Flow

Loading diagram...

API Request Flow

Loading diagram...

State Management

Svelte 5 Runes

BrewHoard uses Svelte 5’s rune-based reactivity:

Svelte
<script>
  // Reactive state
  let beers = $state([]);
  let searchQuery = $state('');
  
  // Derived state (computed values)
  let filteredBeers = $derived(
    beers.filter(b => 
      b.name.toLowerCase().includes(searchQuery.toLowerCase())
    )
  );
  
  // Side effects
  $effect(() => {
    // Runs when dependencies change
    console.log(`Found ${filteredBeers.length} beers`);
  });
  
  // Cleanup on unmount
  $effect(() => {
    const interval = setInterval(checkUpdates, 30000);
    return () => clearInterval(interval);
  });
</script>

Server State (Load Functions)

Data fetched in +page.server.js is passed to components via props:

JavaScript
// +page.server.js
export async function load({ locals }) {
  const collection = await getCollection(locals.user.id);
  return { collection };
}
Svelte
<!-- +page.svelte -->
<script>
  let { data } = $props();
  // data.collection is available here
</script>

Form Actions

Mutations use SvelteKit form actions for progressive enhancement:

JavaScript
// +page.server.js
export const actions = {
  addBeer: async ({ request, locals }) => {
    const formData = await request.formData();
    const beerId = formData.get('beerId');
    
    await addToCollection(locals.user.id, beerId);
    
    return { success: true };
  }
};
Svelte
<!-- +page.svelte -->
<form method="POST" action="?/addBeer">
  <input type="hidden" name="beerId" value={beer.id} />
  <button type="submit">Add to Collection</button>
</form>

Component Architecture

Component Organization

Components are organized by feature and reusability:

Text
components/
├── auth/                    # Auth feature components
│   ├── LoginForm.svelte
│   └── RegisterForm.svelte
├── collection/              # Collection feature
│   ├── AddBeerForm.svelte
│   ├── CollectionGrid.svelte
│   └── CollectionItem.svelte
└── ...

lib/components/
├── ui/                      # shadcn-svelte (generic)
│   ├── button/
│   ├── card/
│   └── ...
└── docs/                    # Documentation components

Props Pattern

Components use the $props() rune with JSDoc for type safety:

Svelte
<script>
  /**
   * @typedef {Object} Props
   * @property {import('$lib/types').Beer} beer - The beer to display
   * @property {boolean} [showActions=true] - Whether to show action buttons
   * @property {(id: string) => void} [onSelect] - Selection callback
   */
  
  /** @type {Props} */
  let { beer, showActions = true, onSelect } = $props();
</script>

Error Handling

API Error Responses

All API endpoints follow a consistent error format:

JavaScript
// Successful response
return json({ 
  success: true, 
  data: result 
});

// Error response
return json({ 
  success: false, 
  error: {
    code: 'VALIDATION_ERROR',
    message: 'Invalid beer ID',
    details: { field: 'beerId' }
  }
}, { status: 400 });

Error Boundaries

SvelteKit’s +error.svelte files catch and display errors:

Svelte
<!-- src/routes/+error.svelte -->
<script>
  import { page } from '$app/state';
</script>

<div class="error-page">
  <h1>{page.status}</h1>
  <p>{page.error?.message}</p>
</div>

Security Considerations

Authentication Flow

Loading diagram...

Authorization Patterns

JavaScript
// Resource-level authorization
export async function GET({ params, locals }) {
  const item = await getCollectionItem(params.id);
  
  // Check ownership
  if (item.user_id !== locals.user.id) {
    throw error(403, 'Access denied');
  }
  
  return json(item);
}

Performance Patterns

Database Query Optimization

JavaScript
// BAD: N+1 query problem
const beers = await sql`SELECT * FROM beers`;
for (const beer of beers) {
  beer.brewery = await sql`
    SELECT * FROM breweries WHERE id = ${beer.brewery_id}
  `;
}

// GOOD: Single query with JOIN
const beers = await sql`
  SELECT b.*, row_to_json(br.*) as brewery
  FROM beers b
  LEFT JOIN breweries br ON b.brewery_id = br.id
`;

Lazy Loading

Large components load dynamically:

Svelte
<script>
  import { onMount } from 'svelte';
  
  let AnalyticsDashboard;
  
  onMount(async () => {
    const module = await import('$components/analytics/AnalyticsDashboard.svelte');
    AnalyticsDashboard = module.default;
  });
</script>

{#if AnalyticsDashboard}
  <AnalyticsDashboard />
{:else}
  <div>Loading analytics...</div>
{/if}

Next Steps