glam/docs/MULTI_FRONTEND_ARCHITECTURE.md
2025-12-21 00:01:54 +01:00

1079 lines
28 KiB
Markdown

# Multi-Frontend Architecture Plan
## Overview
This document outlines the architecture for managing **one shared backend** with **two separate frontends**:
| Frontend | Domain | Purpose |
|----------|--------|---------|
| **Heritage Custodian Portal** | `bronhouder.nl` | Heritage custodian management, ontology visualization, SPARQL queries |
| **ArchiefAssistent (AA)** | `archief.support` | National Archives services, archival research assistance |
## Current State
### Infrastructure
- **Server**: Hetzner Cloud CX22 (`91.98.224.44`)
- **Web Server**: Caddy (automatic HTTPS, reverse proxy)
- **Deployment**: Local SSH/rsync via `deploy.sh`
- **Multi-domain**: Already supported in Caddyfile
### Backend Services (Shared)
| Service | Port | Purpose |
|---------|------|---------|
| Oxigraph | 7878 | SPARQL triplestore |
| FastAPI | 8000 | General API |
| PostgreSQL API | 8001 | Postgres queries |
| Geo API | 8002 | PostGIS geographic data |
| TypeDB API | 8003 | Graph database |
| DuckLake | 8765 | Time travel analytics |
| Qdrant | 6333 | Vector search |
| Valkey | 8090 | Semantic cache |
### Current Frontend
- **Framework**: React 19 + TypeScript + Vite
- **UI Library**: MUI (Material-UI)
- **State**: React Context + TanStack Query
- **Visualization**: D3, MapLibre GL, Mermaid, Three.js
---
## Proposed Architecture
### Option 1: Monorepo with Shared Packages (RECOMMENDED)
```
glam/
├── packages/ # Shared packages
│ ├── ui/ # Shared UI components
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── Button/
│ │ │ │ ├── Card/
│ │ │ │ ├── DataTable/
│ │ │ │ ├── ErrorBoundary/
│ │ │ │ ├── Layout/
│ │ │ │ ├── Navigation/
│ │ │ │ └── Tooltip/
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── hooks/ # Shared React hooks
│ │ ├── src/
│ │ │ ├── useApi.ts
│ │ │ ├── useSparql.ts
│ │ │ ├── useGeocoding.ts
│ │ │ ├── useVectorSearch.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── api-client/ # Shared API client
│ │ ├── src/
│ │ │ ├── sparql.ts
│ │ │ ├── geo.ts
│ │ │ ├── rag.ts
│ │ │ ├── ducklake.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── visualizations/ # Shared visualization components
│ │ ├── src/
│ │ │ ├── Graph/
│ │ │ ├── Map/
│ │ │ ├── UML/
│ │ │ ├── RDF/
│ │ │ └── Timeline/
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── theme/ # Shared theming (but customizable)
│ ├── src/
│ │ ├── base.ts # Base MUI theme
│ │ ├── bronhouder.ts # bronhouder.nl theme overrides
│ │ ├── archief.ts # archief.support theme overrides
│ │ └── index.ts
│ ├── package.json
│ └── tsconfig.json
├── apps/ # Individual frontends
│ ├── bronhouder/ # bronhouder.nl frontend
│ │ ├── src/
│ │ │ ├── pages/ # Site-specific pages
│ │ │ ├── components/ # Site-specific components
│ │ │ ├── contexts/ # Site-specific contexts
│ │ │ ├── App.tsx
│ │ │ └── main.tsx
│ │ ├── public/
│ │ ├── index.html
│ │ ├── vite.config.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── archief-assistent/ # archief.support frontend
│ ├── src/
│ │ ├── pages/ # AA-specific pages
│ │ ├── components/ # AA-specific components
│ │ ├── contexts/ # AA-specific contexts
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── public/
│ ├── index.html
│ ├── vite.config.ts
│ ├── package.json
│ └── tsconfig.json
├── backend/ # Unchanged - shared backend
└── infrastructure/ # Updated for multi-site
├── deploy.sh # Updated with --site flag
└── caddy/
└── Caddyfile # Multi-domain configuration
```
### Why Monorepo with Shared Packages?
| Benefit | Description |
|---------|-------------|
| **Code Reuse** | Share UI components, hooks, and API clients across both frontends |
| **Consistency** | Single source of truth for shared logic |
| **Independent Deployment** | Deploy frontends separately without affecting each other |
| **Customization** | Each frontend can override/extend shared components |
| **Type Safety** | Shared TypeScript types across packages |
| **Testing** | Test shared components once, use everywhere |
---
## Implementation Plan
### Phase 1: Monorepo Setup (Week 1)
#### 1.1 Install Workspace Tool
We'll use **pnpm workspaces** (faster than npm/yarn, excellent monorepo support):
```bash
# Install pnpm globally
npm install -g pnpm
# Initialize workspace in project root
cd /Users/kempersc/apps/glam
```
Create `pnpm-workspace.yaml`:
```yaml
packages:
- 'packages/*'
- 'apps/*'
```
Update root `package.json`:
```json
{
"name": "glam-monorepo",
"private": true,
"scripts": {
"dev:bronhouder": "pnpm --filter @glam/bronhouder dev",
"dev:archief": "pnpm --filter @glam/archief-assistent dev",
"build:all": "pnpm -r build",
"build:bronhouder": "pnpm --filter @glam/bronhouder build",
"build:archief": "pnpm --filter @glam/archief-assistent build",
"test:all": "pnpm -r test",
"lint:all": "pnpm -r lint"
},
"devDependencies": {
"typescript": "~5.9.3"
}
}
```
#### 1.2 Create Shared Packages
**packages/ui/package.json**:
```json
{
"name": "@glam/ui",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./components/*": "./src/components/*/index.ts"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@mui/material": "^7.0.0",
"@emotion/react": "^11.0.0",
"@emotion/styled": "^11.0.0"
},
"devDependencies": {
"@types/react": "^19.2.5",
"typescript": "~5.9.3"
}
}
```
**packages/hooks/package.json**:
```json
{
"name": "@glam/hooks",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"peerDependencies": {
"react": "^19.0.0",
"@tanstack/react-query": "^5.0.0",
"axios": "^1.0.0"
}
}
```
**packages/api-client/package.json**:
```json
{
"name": "@glam/api-client",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"axios": "^1.13.2"
}
}
```
### Phase 2: Extract Shared Components (Week 2)
#### 2.1 Identify Shared vs. Site-Specific Components
**Shared Components** (move to `packages/ui`):
| Component | Current Location | Shared Because |
|-----------|------------------|----------------|
| `ErrorBoundary` | `components/common/` | Universal error handling |
| `LazyLoadError` | `components/common/` | Code splitting support |
| `Tooltip` | `components/ui/` | Generic UI primitive |
| `SearchableMultiSelect` | `components/ui/` | Generic form control |
| `Layout` | `components/layout/` | App shell (customizable) |
| `Navigation` | `components/layout/` | Navigation (customizable menu) |
| `DataTable` | (new) | Tabular data display |
| `Card` | (new) | Content container |
| `Modal` | (new) | Dialog/modal wrapper |
**Shared Visualization Components** (move to `packages/visualizations`):
| Component | Current Location | Shared Because |
|-----------|------------------|----------------|
| `Graph*` | `components/graph/` | Knowledge graph visualization |
| `Map*` | `components/map/` | Geographic visualization |
| `UML*` | `components/uml/` | Schema/ontology diagrams |
| `RDF*` | `components/rdf/` | RDF data visualization |
| `Timeline` | (new) | Temporal data display |
**Site-Specific Components**:
| Component | Site | Purpose |
|-----------|------|---------|
| `CustodianCard` | bronhouder.nl | Heritage institution display |
| `OntologyExplorer` | bronhouder.nl | LinkML schema browser |
| `SparqlWorkbench` | bronhouder.nl | Query interface |
| `ArchiveSearch` | archief.support | Archival record search |
| `ResearchAssistant` | archief.support | AI-powered research help |
| `FamilyTreeViewer` | archief.support | Genealogical data viz |
#### 2.2 Component Extraction Pattern
**Before** (monolithic):
```tsx
// frontend/src/components/common/ErrorBoundary.tsx
export class ErrorBoundary extends React.Component { ... }
```
**After** (package):
```tsx
// packages/ui/src/components/ErrorBoundary/ErrorBoundary.tsx
export class ErrorBoundary extends React.Component { ... }
// packages/ui/src/components/ErrorBoundary/index.ts
export { ErrorBoundary } from './ErrorBoundary';
export type { ErrorBoundaryProps } from './ErrorBoundary';
// packages/ui/src/index.ts
export * from './components/ErrorBoundary';
export * from './components/Tooltip';
// ... etc
```
**Usage in app**:
```tsx
// apps/bronhouder/src/App.tsx
import { ErrorBoundary, Tooltip, Layout } from '@glam/ui';
import { useSparql, useGeocoding } from '@glam/hooks';
import { sparqlClient, geoClient } from '@glam/api-client';
```
### Phase 3: Create Site Frontends (Week 3)
#### 3.1 bronhouder.nl Frontend
**apps/bronhouder/package.json**:
```json
{
"name": "@glam/bronhouder",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@glam/ui": "workspace:*",
"@glam/hooks": "workspace:*",
"@glam/api-client": "workspace:*",
"@glam/visualizations": "workspace:*",
"@glam/theme": "workspace:*",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"@mui/material": "^7.3.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@tanstack/react-query": "^5.90.10"
}
}
```
**apps/bronhouder/src/App.tsx**:
```tsx
import { ThemeProvider } from '@mui/material';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Layout, ErrorBoundary } from '@glam/ui';
import { bronhouderTheme } from '@glam/theme';
// Site-specific pages
import { HomePage } from './pages/Home';
import { CustodianExplorer } from './pages/CustodianExplorer';
import { OntologyViewer } from './pages/OntologyViewer';
import { SparqlWorkbench } from './pages/SparqlWorkbench';
const queryClient = new QueryClient();
export function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={bronhouderTheme}>
<BrowserRouter>
<Layout
siteName="Bronhouder"
logo="/assets/bronhouder-logo.svg"
navigation={[
{ label: 'Home', path: '/' },
{ label: 'Custodians', path: '/custodians' },
{ label: 'Ontology', path: '/ontology' },
{ label: 'SPARQL', path: '/sparql' },
]}
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/custodians/*" element={<CustodianExplorer />} />
<Route path="/ontology/*" element={<OntologyViewer />} />
<Route path="/sparql" element={<SparqlWorkbench />} />
</Routes>
</Layout>
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
</ErrorBoundary>
);
}
```
#### 3.2 archief.support Frontend (ArchiefAssistent)
**apps/archief-assistent/package.json**:
```json
{
"name": "@glam/archief-assistent",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@glam/ui": "workspace:*",
"@glam/hooks": "workspace:*",
"@glam/api-client": "workspace:*",
"@glam/visualizations": "workspace:*",
"@glam/theme": "workspace:*",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"@mui/material": "^7.3.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@tanstack/react-query": "^5.90.10"
}
}
```
**apps/archief-assistent/src/App.tsx**:
```tsx
import { ThemeProvider } from '@mui/material';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Layout, ErrorBoundary } from '@glam/ui';
import { archiefTheme } from '@glam/theme';
// Site-specific pages
import { HomePage } from './pages/Home';
import { SearchPage } from './pages/Search';
import { AssistantPage } from './pages/Assistant';
import { CollectionsPage } from './pages/Collections';
import { GenealogyPage } from './pages/Genealogy';
const queryClient = new QueryClient();
export function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={archiefTheme}>
<BrowserRouter>
<Layout
siteName="ArchiefAssistent"
logo="/assets/archief-logo.svg"
navigation={[
{ label: 'Home', path: '/' },
{ label: 'Zoeken', path: '/zoeken' },
{ label: 'Assistent', path: '/assistent' },
{ label: 'Collecties', path: '/collecties' },
{ label: 'Stamboom', path: '/stamboom' },
]}
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/zoeken/*" element={<SearchPage />} />
<Route path="/assistent" element={<AssistantPage />} />
<Route path="/collecties/*" element={<CollectionsPage />} />
<Route path="/stamboom/*" element={<GenealogyPage />} />
</Routes>
</Layout>
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
</ErrorBoundary>
);
}
```
### Phase 4: Theming Strategy (Week 3)
#### 4.1 Base Theme
**packages/theme/src/base.ts**:
```tsx
import { createTheme, ThemeOptions } from '@mui/material';
export const baseThemeOptions: ThemeOptions = {
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
h1: { fontSize: '2.5rem', fontWeight: 600 },
h2: { fontSize: '2rem', fontWeight: 600 },
// ... etc
},
shape: {
borderRadius: 8,
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
},
},
},
MuiCard: {
styleOverrides: {
root: {
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
},
},
},
},
};
export const baseTheme = createTheme(baseThemeOptions);
```
#### 4.2 Site-Specific Themes
**packages/theme/src/bronhouder.ts**:
```tsx
import { createTheme, ThemeOptions } from '@mui/material';
import { baseThemeOptions } from './base';
import { deepmerge } from '@mui/utils';
const bronhouderOverrides: ThemeOptions = {
palette: {
primary: {
main: '#1976d2', // Blue - professional, data-focused
light: '#42a5f5',
dark: '#1565c0',
},
secondary: {
main: '#7c4dff', // Purple - ontology/knowledge
light: '#b47cff',
dark: '#3f1dcb',
},
background: {
default: '#f5f7fa',
paper: '#ffffff',
},
},
};
export const bronhouderTheme = createTheme(
deepmerge(baseThemeOptions, bronhouderOverrides)
);
```
**packages/theme/src/archief.ts**:
```tsx
import { createTheme, ThemeOptions } from '@mui/material';
import { baseThemeOptions } from './base';
import { deepmerge } from '@mui/utils';
const archiefOverrides: ThemeOptions = {
palette: {
primary: {
main: '#2e7d32', // Green - archival, heritage
light: '#60ad5e',
dark: '#005005',
},
secondary: {
main: '#ff8f00', // Orange - attention, assistance
light: '#ffc046',
dark: '#c56000',
},
background: {
default: '#fafaf5', // Warm paper-like background
paper: '#ffffff',
},
},
};
export const archiefTheme = createTheme(
deepmerge(baseThemeOptions, archiefOverrides)
);
```
### Phase 5: Infrastructure Updates (Week 4)
#### 5.1 Updated Caddyfile
**infrastructure/caddy/Caddyfile**:
```caddy
# Shared API routes snippet
(api_routes) {
# DuckLake - Time travel analytics
handle /ducklake/* {
reverse_proxy 127.0.0.1:8765
}
# FastAPI general API
handle /api/* {
reverse_proxy 127.0.0.1:8000
}
# PostgreSQL API
handle /api/postgres/* {
reverse_proxy 127.0.0.1:8001
}
# Geographic API
handle /api/geo/* {
reverse_proxy 127.0.0.1:8002
}
# TypeDB API
handle /api/typedb/* {
reverse_proxy 127.0.0.1:8003
}
# RAG API
handle /api/rag/* {
reverse_proxy 127.0.0.1:8004
}
# Semantic Cache
handle /api/cache/* {
reverse_proxy 127.0.0.1:8090
}
# Qdrant vector database
handle /qdrant/* {
reverse_proxy 127.0.0.1:6333
}
# SPARQL endpoint
handle /query {
reverse_proxy 127.0.0.1:7878
}
handle /sparql/* {
uri strip_prefix /sparql
reverse_proxy 127.0.0.1:7878
}
}
# Static file serving snippet (parameterized)
(static_files) {
try_files {path} /index.html
file_server
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
}
}
# =============================================================================
# bronhouder.nl - Heritage Custodian Portal
# =============================================================================
bronhouder.nl, www.bronhouder.nl {
log {
output file /var/log/caddy/bronhouder.log
format json
}
# Import shared API routes
import api_routes
# Health check
handle /health {
respond "OK" 200
}
# Static frontend files
handle {
root * /var/www/bronhouder
import static_files
}
}
# SPARQL subdomain for bronhouder
sparql.bronhouder.nl {
reverse_proxy 127.0.0.1:7878 {
header_up Host {http.request.host}
header_up X-Real-IP {http.request.remote.host}
}
}
# =============================================================================
# archief.support - ArchiefAssistent (National Archives Services)
# =============================================================================
archief.support, www.archief.support {
log {
output file /var/log/caddy/archief-support.log
format json
}
# Import shared API routes
import api_routes
# Health check
handle /health {
respond "OK" 200
}
# Static frontend files
handle {
root * /var/www/archief-assistent
import static_files
}
}
# API subdomain for archief.support (optional)
api.archief.support {
import api_routes
handle {
respond "ArchiefAssistent API" 200
}
}
```
#### 5.2 Updated deploy.sh
**infrastructure/deploy.sh** (additions):
```bash
#!/bin/bash
set -euo pipefail
# ... existing code ...
BRONHOUDER_FRONTEND_PATH="/var/www/bronhouder"
ARCHIEF_FRONTEND_PATH="/var/www/archief-assistent"
# Parse --site argument
SITE=""
while [[ $# -gt 0 ]]; do
case $1 in
--site=*)
SITE="${1#*=}"
shift
;;
--frontend)
DEPLOY_FRONTEND=true
shift
;;
# ... other flags ...
esac
done
deploy_frontend() {
local site="$1"
local source_dir=""
local target_path=""
case "$site" in
bronhouder)
source_dir="apps/bronhouder"
target_path="$BRONHOUDER_FRONTEND_PATH"
;;
archief|archief-assistent)
source_dir="apps/archief-assistent"
target_path="$ARCHIEF_FRONTEND_PATH"
;;
all)
deploy_frontend "bronhouder"
deploy_frontend "archief-assistent"
return
;;
*)
echo "Unknown site: $site"
echo "Valid sites: bronhouder, archief-assistent, all"
exit 1
;;
esac
echo "=== Deploying $site frontend ==="
# Build
echo "Building $site..."
cd "$source_dir"
pnpm install
pnpm build
# Deploy
echo "Deploying to $target_path..."
rsync -avz --delete dist/ "root@${SERVER_IP}:${target_path}/"
cd ../..
echo "=== $site frontend deployed ==="
}
# In main logic
if [[ "$DEPLOY_FRONTEND" == "true" ]]; then
if [[ -z "$SITE" ]]; then
echo "Error: --frontend requires --site=<name>"
echo "Usage: ./deploy.sh --frontend --site=bronhouder"
echo " ./deploy.sh --frontend --site=archief-assistent"
echo " ./deploy.sh --frontend --site=all"
exit 1
fi
deploy_frontend "$SITE"
fi
```
**Usage**:
```bash
# Deploy bronhouder.nl frontend only
./infrastructure/deploy.sh --frontend --site=bronhouder
# Deploy archief.support frontend only
./infrastructure/deploy.sh --frontend --site=archief-assistent
# Deploy both frontends
./infrastructure/deploy.sh --frontend --site=all
```
---
## Component Sharing Patterns
### Pattern 1: Polymorphic Components
Design components that adapt based on props:
```tsx
// packages/ui/src/components/Layout/Layout.tsx
interface LayoutProps {
siteName: string;
logo: string;
navigation: NavItem[];
children: React.ReactNode;
variant?: 'default' | 'compact' | 'fullwidth';
footer?: React.ReactNode;
}
export function Layout({
siteName,
logo,
navigation,
children,
variant = 'default',
footer
}: LayoutProps) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<Navigation siteName={siteName} logo={logo} items={navigation} />
<Box component="main" sx={layoutVariants[variant]}>
{children}
</Box>
{footer ?? <DefaultFooter siteName={siteName} />}
</Box>
);
}
```
### Pattern 2: Composition Over Inheritance
Create building blocks that can be composed differently:
```tsx
// packages/ui/src/components/Card/Card.tsx
export function Card({ children, ...props }: CardProps) { ... }
export function CardHeader({ title, subtitle, actions }: CardHeaderProps) { ... }
export function CardContent({ children }: CardContentProps) { ... }
export function CardActions({ children }: CardActionsProps) { ... }
// apps/bronhouder - Custodian card
import { Card, CardHeader, CardContent } from '@glam/ui';
export function CustodianCard({ custodian }) {
return (
<Card>
<CardHeader
title={custodian.name}
subtitle={custodian.type}
actions={<BookmarkButton />}
/>
<CardContent>
<CustodianDetails custodian={custodian} />
</CardContent>
</Card>
);
}
// apps/archief-assistent - Archive record card
import { Card, CardHeader, CardContent } from '@glam/ui';
export function ArchiveRecordCard({ record }) {
return (
<Card>
<CardHeader
title={record.title}
subtitle={record.dateRange}
actions={<RequestButton />}
/>
<CardContent>
<RecordMetadata record={record} />
</CardContent>
</Card>
);
}
```
### Pattern 3: Render Props / Slots
Allow customization through slots:
```tsx
// packages/visualizations/src/Graph/Graph.tsx
interface GraphProps<T> {
data: T[];
renderNode?: (node: T) => React.ReactNode;
renderEdge?: (edge: Edge) => React.ReactNode;
renderTooltip?: (node: T) => React.ReactNode;
}
export function Graph<T>({
data,
renderNode = DefaultNode,
renderEdge = DefaultEdge,
renderTooltip = DefaultTooltip
}: GraphProps<T>) {
// Graph implementation using D3/ELK
return (
<svg>
{edges.map(edge => renderEdge(edge))}
{nodes.map(node => renderNode(node))}
</svg>
);
}
```
### Pattern 4: Feature Flags
Control feature visibility per site:
```tsx
// packages/ui/src/contexts/FeatureFlagsContext.tsx
interface FeatureFlags {
sparqlWorkbench: boolean;
genealogyTools: boolean;
aiAssistant: boolean;
advancedFilters: boolean;
}
const defaultFlags: Record<string, FeatureFlags> = {
bronhouder: {
sparqlWorkbench: true,
genealogyTools: false,
aiAssistant: false,
advancedFilters: true,
},
archiefAssistent: {
sparqlWorkbench: false,
genealogyTools: true,
aiAssistant: true,
advancedFilters: true,
},
};
// Usage
import { useFeatureFlags } from '@glam/ui';
function Navigation() {
const { sparqlWorkbench, aiAssistant } = useFeatureFlags();
return (
<nav>
{sparqlWorkbench && <NavLink to="/sparql">SPARQL</NavLink>}
{aiAssistant && <NavLink to="/assistant">AI Assistant</NavLink>}
</nav>
);
}
```
---
## Migration Strategy
### Step 1: Setup (Day 1-2)
1. Install pnpm: `npm install -g pnpm`
2. Create workspace configuration
3. Create package directory structure
4. Set up TypeScript project references
### Step 2: Extract Shared Code (Day 3-5)
1. Move UI components to `packages/ui`
2. Move hooks to `packages/hooks`
3. Move API clients to `packages/api-client`
4. Move visualization components to `packages/visualizations`
5. Update imports in existing frontend
### Step 3: Create Second Frontend (Day 6-8)
1. Create `apps/archief-assistent` scaffold
2. Set up routing and pages
3. Apply archief theme
4. Implement AA-specific features
### Step 4: Infrastructure (Day 9-10)
1. Update Caddyfile for multi-domain
2. Update deploy.sh for multi-site
3. Set up DNS for archief.support
4. Test deployments
### Step 5: Testing & Polish (Day 11-14)
1. E2E tests for both sites
2. Performance optimization
3. Accessibility audit
4. Documentation
---
## Development Workflow
### Daily Development
```bash
# Terminal 1: Develop bronhouder.nl
pnpm dev:bronhouder
# Terminal 2: Develop archief.support
pnpm dev:archief
# Terminal 3: Backend services
./infrastructure/deploy.sh --status
```
### Making Changes to Shared Packages
```bash
# Changes to packages/* are immediately available to apps
# because we use workspace:* references
# If you modify packages/ui/src/components/Button.tsx
# Both apps will see the changes on next hot reload
```
### Deploying
```bash
# Deploy specific site
./infrastructure/deploy.sh --frontend --site=bronhouder
# Deploy both sites
./infrastructure/deploy.sh --frontend --site=all
# Deploy everything (backend + both frontends)
./infrastructure/deploy.sh --all --site=all
```
---
## Summary
| Aspect | Approach |
|--------|----------|
| **Monorepo Tool** | pnpm workspaces |
| **Package Structure** | `packages/*` (shared), `apps/*` (sites) |
| **Shared Components** | `@glam/ui`, `@glam/hooks`, `@glam/api-client`, `@glam/visualizations`, `@glam/theme` |
| **Theming** | Base theme + site-specific overrides |
| **Deployment** | SSH/rsync per site, Caddy multi-domain |
| **Development** | Parallel Vite dev servers |
| **Type Safety** | TypeScript project references |
This architecture provides maximum code reuse while allowing full customization per site, with clear separation of concerns and independent deployment capability.