Architecture & Implementation Decisions

Session: January 30, 2026
Project: fogserv.cloud Website Rebuild

This document captures architectural decisions, technology choices, and implementation patterns used throughout the project, serving as institutional knowledge for future development and agent operations.

Last updated: August 28, 2026 — standalone rootless Podman deployment.


Deployment Architecture (August 28, 2026)

Standalone Rootless Podman Container

Decision: Deploy fogserv.cloud as a single rootless Podman container on the host, with Caddy as a TLS reverse proxy. No k3s, no Rancher, no cluster orchestrator.

Rationale:

Topology:

Internet (443) → Caddy (rootful, port 443) → 127.0.0.1:8080 (rootless podman) → vite preview (bun)
                                            ↓
                                       ~/fogserv.db (host volume mount → /app/dev.db)

Container command:

podman run -d --name fogserv-cloud \
  -p 127.0.0.1:8080:8080 \
  --restart=always \
  -v ~/fogserv.db:/app/dev.db \
  -e DATABASE_URL='file:./dev.db' \
  -e NODE_ENV=production \
  ghcr.io/fogserv/fogserv-cloud:latest

Caddy config (fogserv.cloud):

fogserv.cloud {
    tls internal             # temporary, bypasses ACME rate limit
    reverse_proxy 127.0.0.1:8080
    encode zstd
    header {
        Cache-Control "public, max-age=86400"
    }
}

Auto-start on reboot (rootless):

podman generate systemd --new --name fogserv-cloud \
  > ~/.config/systemd/user/fogserv-cloud.service
systemctl --user daemon-reload
systemctl --user enable --now fogserv-cloud.service

Mailgun env vars (required in container):

CI/CD: .github/workflows/deploy-site.yml builds the image, pushes to GHCR, SSHes to the host and runs podman pull && podman restart (or compose up).

Legacy: k3s / Enterprise/

/Enterprise/ directory and deploy-enterprise.yml are kept for reference but no longer part of the active deployment path. deploy-enterprise.yml is marked TESTING / DISABLED (manual workflow_dispatch only).


Technology Stack

Core Framework: React 19 + TanStack + Vite

Decision: Use modern React with TanStack ecosystem
Rationale:

Alternatives Considered:

Trade-offs:


Package Manager: Bun

Decision: Use Bun for all package management and runtime
Rationale:

Alternatives Considered:

Trade-offs:


Database: PostgreSQL + Prisma + Accelerate

Decision: Use Prisma ORM with Accelerate connection pooling
Rationale:

Architecture:

Application → Prisma Client → Prisma Accelerate → PostgreSQL
                                    ↓
                              Connection Pool
                              Edge Caching
                              Query Optimization

Environment Variables:

Alternatives Considered:

Trade-offs:


Styling: Tailwind CSS v4

Decision: Use Tailwind v4 with new PostCSS architecture
Rationale:

Configuration:

// tailwind.config.ts
export default {
  content: ["./src/**/*.{ts,tsx}"],
  theme: { extend: { ... } },
}

// src/index.css
@import "tailwindcss";

Alternatives Considered:

Trade-offs:


Secrets Management: dotenvx

Decision: Use dotenvx for all environment variable handling
Rationale:

Pattern:

{
  "scripts": {
    "dev": "dotenvx run -- vite",
    "build": "dotenvx run -- vite build"
  }
}

Alternatives Considered:

Trade-offs:


Application Architecture

Routing Strategy

Decision: Single-file router configuration with inline components (MVP)
Future: Migrate to file-based routing with @tanstack/router-plugin

Current Structure:

src/
  main.tsx          # Router config + all page components
  index.css         # Global styles
  routes/
    __root.tsx      # Legacy, kept for reference
    index.tsx       # Legacy, kept for reference

Rationale:

Future Migration Path:

src/
  routes/
    __root.tsx      # Layout with <Outlet />
    index.tsx       # HomePage
    about.tsx       # AboutPage
    blog/
      index.tsx     # BlogPage
      $slug.tsx     # Individual post
    kb/
      index.tsx     # KnowledgeBasePage
      $doc.tsx      # Individual KB document

Component Patterns

Navigation Component:

Footer Component:

Page Components:

Card Components:


State Management

Current: React component state only
Future: TanStack Query for server state

Rationale:

Pattern:

// Server state (future)
const { data, isLoading } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

// Form state (future)
const { register, handleSubmit } = useForm()

// URL state
const { slug } = useParams({ from: '/blog/$slug' })

Database Schema Design

Current Schema (Prisma)

The schema is now expanded and active across CMS, CRM, and analytics domains.

Auth and users:

CMS models:

CRM and email models:

Analytics models:

Migration Baseline

The first migration has been created and applied for the current schema foundation:

This establishes the baseline for future incremental migrations tied to feature delivery.


API Design (Future)

Email Service Abstraction (Implemented Foundation)

An email service layer now exists at src/services/emailService.ts with two execution modes:

The service exposes:

Current transactional templates in the foundation:

Runtime behavior is controlled by typed Vite env vars:

This keeps Mailgun secrets out of frontend code while allowing integration to be wired through server routes.

Transactional Email Backend Wiring (Implemented)

Backend API wiring for transactional send is implemented at:

Current implementation details:

Admin-level end-to-end test path:

Email Verification Flow (Implemented)

Verification endpoints are now implemented and connected to subscriber state:

Verification request behavior:

Verification confirm behavior:

Client integration:

Mailgun Webhook Ingestion (Implemented)

Webhook endpoint:

Implementation details:

Subscriber-state mapping currently applied:

This provides immediate lifecycle tracking for subscriber health while campaign-level correlation remains a follow-up task.

RESTful Endpoints

GET    /api/posts              # List all posts
GET    /api/posts/:slug        # Get single post
POST   /api/posts              # Create post (auth)
PATCH  /api/posts/:id          # Update post (auth)
DELETE /api/posts/:id          # Delete post (auth)

POST   /api/subscribe          # Newsletter signup
POST   /api/unsubscribe        # Unsubscribe
POST   /api/contact            # Contact form

GET    /api/kb                 # List KB documents
GET    /api/kb/:slug           # Get KB document

API Route Structure (Vite/TanStack)

src/
  api/
    posts.ts
    subscribe.ts
    kb.ts

Security Patterns

Environment Variables

Database Security

Authentication (Future)

API Security (Future)


Performance Patterns

Code Splitting

const AdminDashboard = lazy(() => import('./pages/Admin'))

Image Optimization

Database Optimization

Bundle Optimization


Testing Strategy (Future)

Unit Tests

Integration Tests

E2E Tests


Deployment Strategy (Future)

CI/CD Pipeline (Forgejo Actions)

name: Deploy
on:
  push:
    branches: [main]
steps:
  - Checkout code
  - Install dependencies (bun install)
  - Run tests
  - Run database migrations
  - Build production bundle
  - Deploy to server
  - Health check

Deployment Targets

Monitoring


File Organization

Project Structure

fogserv.cloud/
├── src/
│   ├── main.tsx           # Entry point + router
│   ├── index.css          # Global styles
│   └── routes/            # Legacy route files
├── kb/                     # Knowledge Base
│   ├── TELOS.md           # Mission & philosophy
│   ├── tasks.md           # Task tracking
│   ├── lessons-learned.md # Session insights
│   ├── problems-solved.md # Technical issues
│   └── implementations.md # This file
├── prisma/
│   └── schema.prisma      # Database models
├── .env                   # Local secrets (not committed)
├── .env.example           # Template (committed)
├── package.json           # Dependencies + scripts
├── vite.config.ts         # Vite configuration
├── tailwind.config.ts     # Tailwind configuration
└── tsconfig.json          # TypeScript configuration

Naming Conventions

Files

Code

Database


Future Considerations

Internationalization

Analytics

Content Versioning

Multi-tenancy (Far Future)


Last Updated: April 22, 2026
Decisions Made: 20+
Related Files:


Auth System (April 2026)

Session-Based Auth with Native Crypto

Decision: Implement auth using Node.js crypto.scrypt, Prisma Session table, and Bearer tokens. No third-party auth library.

Password storage: salt:derivedKey — 16-byte random salt, 64-byte scrypt key, both hex-encoded.

Session lifecycle: 30-day expiry stored in DB. getSessionUser(req) reads Authorization: Bearer <token>, validates, auto-deletes expired rows.

User enumeration protection: When no user is found, run a dummy verifyPassword call so the response time is constant.

Registration flow:

  1. Validate email + password (≥8 chars)
  2. Check for existing user
  3. Hash password, create User (status PENDING_VERIFICATION)
  4. Create Subscriber record with confirm token, send verification email
  5. Return { ok, userId, email } — no token yet (user must verify email first, then login)

Login flow:

  1. Fetch user by email (or run dummy hash if not found)
  2. timingSafeEqual comparison
  3. Check SUSPENDED status
  4. Create Session, update lastLoginAt
  5. Return { ok, token, user: { id, email, name, role, status, avatar } }

Frontend AuthContext pattern:

// Provider wraps RouterProvider — NOT inside a route component
<AuthProvider>
  <RouterProvider router={router} />
</AuthProvider>

useAuth() exposes: { auth: AuthState, login, logout, register }. Token stored in localStorage under key fogserv_session_token.


Newsletter Double Opt-In (April 2026)

NewsletterSignupCard component: Two variants — compact (footer) and full (homepage section). State machine: idle → loading → success | error.

Verification URL: /verify-email?token=<token>&email=<email> — both params required because the backend confirmVerification needs the email to look up the subscriber when the token alone is ambiguous.

VerifyEmailPage: Reads URL params on mount, auto-calls emailService.confirmVerification({ token, email }), shows loading/success/error states.


Single-File Architecture Notes (src/main.tsx)

The entire frontend (components, routes, services, context) lives in one file (src/main.tsx). This is intentional for this phase of development — avoids premature abstraction.

Ordering convention:

  1. Imports
  2. Auth types + AuthContext + AuthProvider + useAuth
  3. KB data types + glob imports
  4. Utility components (Card, etc.)
  5. Page components (alphabetical within phase groupings)
  6. Auth page components (LoginPage, RegisterPage, ProfilePage)
  7. Route definitions
  8. Router construction + ReactDOM.createRoot

Duplication risk: When summarized context is resumed, the agent may not know a block was already written. Always grep_search for the symbol before inserting.


2026-08-28 — Standalone Podman deployment (replaced k3s/Enterprise)

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse