spomeni/docs/description.md
dimitar 9c354543fc
Some checks are pending
CI / build (push) Waiting to run
docs: Phase 7 — reconcile drift, delete scratch, rewrite admin.md
Reconcile the docs with the shipped implementation. The exploration
flagged three docs as out of sync; one was a scratch file.

- docs/test.md deleted. File content was literally the four bytes
  'aaaa' — a debugging leftover that had no business in the repo.
- docs/admin.md rewritten end-to-end. The old file still described
  the removed hardcoded super/admin credentials ('SuperAdmin will
  log in with hardcoded username: super and password:admin'). The
  new version covers roles, the dual-auth model, the
  requireAdmin/requireAdminPost/requireSuperAdminPost guard flow,
  env-based super-admin provisioning (with the bcrypt-hash generation
  command), code generation + the atomic-claim guard, the
  Code.createdById onDelete:Restrict decision, and the rate-limit
  surface — i.e. all of the Phase 1–6 decisions in one place. It
  is now the authoritative source of truth for the admin flow.
- docs/description.md (the 324-line original architecture plan) gets
  a banner at the top listing every known delta with the actual
  implementation: storage (S3 not UploadThing/Vercel Blob),
  upload MIME allow-list, dual auth (Clerk + custom HMAC), production
  deploy (Coolify/Traefik instead of Vercel), subdomain routing
  (Host header, not X-Subdomain), schema notes for the deliberate
  String? bornDate/passedDate, rate limiting + CSRF enforcement, and
  in-house QR rendering. The body of the document is preserved as
  historical design context.
- docs/adminImplem.md (the 259-line original implementation plan)
  gets a similar banner — most notable is that the cookie path is
  actually '/' not '/admin', and the rolling helpers in
  admin-session.ts supersede the per-route inline guards the doc
  describes. Body preserved as history.

No source changes — docs only.

Note: docs/db.md is left untouched per the user's instruction to defer
secret rotation to a later stage. It still contains an in-repo DB
password; that and the other .env secrets will be addressed when
rotation happens.
2026-08-02 15:15:50 +02:00

15 KiB

🏛️ City Monuments Memories — Platform Architecture & Implementation Plan

⚠ Historical spec — partially out of sync with the implementation

This document is the original architecture plan. The shipped code has drifted from it in several ways; treat this as historical design context, not current documentation.

Known deltas (see docs/admin.md and the source for the source of truth):

  • Storage: described as "UploadThing" + "Vercel Blob"; the implementation uses AWS-compatible S3 (Contabo Object Storage) via @aws-sdk/client-s3. There is an in-house presigned-URL helper in src/lib/upload.ts (currently the upload route still proxies through the server with magic-byte validation; presigned direct-to-S3 flow is implemented but not yet wired into the client).
  • File Upload: 5MB cap, JPEG/PNG/WebP only (GIF was removed).
  • Auth: described as "Clerk only". The implementation uses dual-track auth: Clerk for end-users, plus a custom HMAC-signed cookie for admins (src/lib/admin-session.ts). Admin session secret comes from ADMIN_SESSION_SECRET; super-admin credentials from SUPER_ADMIN_USERNAME + SUPER_ADMIN_PASSWORD_HASH (bcrypt) — previously hardcoded super/admin, now removed.
  • Deployment: described as "Vercel". Actual deploy is Docker (standalone Next.js) behind Traefik (Coolify). The nginx/conf.d/* configs have been deleted.
  • Subdomain routing: described as "Vercel Wildcard Domains"; middleware parses the Host header (not X-Subdomain) behind the Traefik reverse proxy.
  • Schema: see prisma/schema.prisma for the current shape. Most notably bornDate/passedDate are deliberately kept as bounded VARCHAR(50) (imprecise free-text like "1960" or "early 1990s"), not DateTime.
  • Rate limiting + CSRF are now enforced on admin + sensitive public routes (see src/lib/rate-limit.ts).
  • QR codes are rendered in-house via the qrcode package, not via the external api.qrserver.com service.

1. Tech Stack

Layer Technology Why
Framework Next.js 15 (App Router) Full-stack React framework — server components, API routes, middleware for subdomain routing
Auth Clerk Required by spec; handles sign-up/sign-in, user profiles, session management
Database PostgreSQL + Prisma ORM Relational data model (users, monuments, memories); Prisma for type-safe queries & migrations
File Upload UploadThing Built for Next.js, handles image resizing, CDN delivery, max 3 files validation
QR Code qrcode (npm) Pure JS, generates PNG/SVG from server, 1M+ weekly downloads
Subdomain Next.js Middleware + Vercel Wildcard Domains *.monuments.app rewrites to dynamic route [subdomain]/page.tsx
Templates React Server Components + Tailwind CSS 3 pre-designed HTML landing page templates as React components; server-rendered for SEO
Deployment Vercel Native Next.js support, wildcard domains, serverless functions, Edge middleware
Storage Vercel Blob / AWS S3 For uploaded monument images

2. Data Model (Prisma Schema)

model User {
  id            String    @id @default(cuid())
  clerkId       String    @unique
  email         String?
  name          String?
  subdomain     String    @unique          // e.g. "eiffel-tower"
  templateId    Int                          // 1, 2, or 3
  title         String?                      // monument page title
  description   String?                      // user-written text
  published     Boolean   @default(false)
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  images        Image[]
}

model Image {
  id        String   @id @default(cuid())
  url       String                        // CDN URL from UploadThing
  key       String                        // UploadThing file key
  order     Int                           // display order (1-3)
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
}

3. Application Flow

                   ┌─────────────────────────┐
                   │  monuments.app (landing) │
                   └────────┬────────────────┘
                            │ Sign Up via Clerk
                            ▼
              ┌─────────────────────────────┐
              │   Dashboard / Profile Setup  │
              │   - Pick template (1/2/3)    │
              │   - Enter monument name/text  │
              │   - Upload up to 3 photos     │
              │   - Choose custom subdomain   │
              └─────────────┬───────────────┘
                            │ Publish
                            ▼
        ┌────────────────────────────────────────┐
        │  1. Generate subdomain + HTML landing   │
        │  2. Generate QR code (→ subdomain URL)  │
        │  3. Store everything in DB              │
        └────────────────┬───────────────────────┘
                         │
                         ▼
        ┌────────────────────────────────────────┐
        │  eiffel-tower.monuments.app            │
        │  → SSR landing page from template      │
        │  → Displays text + 3 photos            │
        └────────────────────────────────────────┘

4. Implementation Plan — Phase by Phase

Phase 0: Project Setup (Day 1)

npx create-next-app@latest city-monuments --typescript --tailwind --app
npm install @clerk/nextjs prisma @prisma/client @uploadthing/react uploadthing qrcode qrcode @types/qrcode
  • Create Next.js 15 project with TypeScript + Tailwind
  • Configure Clerk (.env.local with publishable + secret keys)
  • Set up app/layout.tsx with <ClerkProvider>
  • Initialize Prisma with PostgreSQL
  • Set up UploadThing with file router (max 3 images, 5MB each, image only)

Phase 1: Authentication & Onboarding (Days 2-3)

  • Configure Clerk middleware (src/middleware.ts)
  • Create /sign-in and /sign-up pages using Clerk components
  • Create /dashboard — protected page showing user's monument (or "create one" CTA)
  • Create /onboarding wizard page:
    • Step 1: Enter monument name and description (text area)
    • Step 2: Upload up to 3 photos (UploadThing dropzone, drag-to-reorder)
    • Step 3: Choose subdomain slug (check availability via API)
    • Step 4: Pick template (3 visual card selections)
  • On submit: call /api/publish to save everything

Phase 2: API Routes (Days 3-4)

Route Method Purpose
/api/publish POST Saves user data, generates subdomain, returns QR code
/api/check-subdomain GET ?slug=xyz — returns {available: bool}
/api/monument/[subdomain] GET Public JSON data for a monument page
/api/upload POST UploadThing endpoint (auto-generated)
/api/user/monument GET/PUT Get/update current user's monument data

Key logic in /api/publish:

  1. Validate input (text length, image count, template ID)
  2. Check subdomain availability
  3. Store in DB (User + Image records)
  4. Generate QR code via qrcode.toBuffer(subdomainUrl, { type: 'png' })
  5. Return QR code as base64 data URL + public monument URL
  6. (Optional) Store QR code image in blob storage

Phase 3: Subdomain Routing & Middleware (Day 4)

// src/middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export default clerkMiddleware(async (auth, req: NextRequest) => {
  const url = req.nextUrl
  const hostname = req.headers.get('host') || ''
  
  // Extract subdomain (e.g. "eiffel-tower" from "eiffel-tower.monuments.app")
  const subdomain = hostname
    .replace('www.', '')
    .replace('.monuments.app', '')
  
  // If it's a subdomain (not apex), rewrite to the dynamic page
  if (subdomain && !subdomain.includes('.') && hostname.includes('.monuments.app')) {
    url.pathname = `/${subdomain}`
    return NextResponse.rewrite(url)
  }
  
  return NextResponse.next()
})

export const config = {
  matcher: ['/((?!_next|api|static|.*\\..*).*)']
}
  • Create app/[subdomain]/page.tsx — fetches monument data, renders template
  • Handle 404 for unknown subdomains
  • Configure Vercel: add *.monuments.app as wildcard domain

Phase 4: Template System (Day 5)

Create 3 React Server Components that render pure HTML landing pages:

Template Vibe Layout
Template 1 — "Classic" Clean, serif font, historical archive feel Header image hero → description → 3-column photo grid
Template 2 — "Modern" Bold, full-bleed images, sans-serif Full-screen photo carousel → floating text overlay → gallery
Template 3 — "Minimal" Whitespace-heavy, single-column, journal style Title → paragraph → horizontal image strip → footer

All templates:

  • Are React Server Components (no JS shipped for visitors!)
  • Use Tailwind CSS for styling
  • Are SEO-friendly with proper <head> meta tags
  • Render optimized images via Next.js <Image> component
  • Include the QR code badge in the footer ("Scan to visit this monument")
  • Support Open Graph metadata for social sharing

Example template rendering function:

// src/lib/templates.tsx
export function renderTemplate(templateId: number, data: MonumentData) {
  switch (templateId) {
    case 1: return <TemplateClassic data={data} />
    case 2: return <TemplateModern data={data} />
    case 3: return <TemplateMinimal data={data} />
  }
}

Phase 5: QR Code Generation (Day 5)

// src/lib/qrcode.ts
import QRCode from 'qrcode'

export async function generateMonumentQR(subdomain: string): Promise<string> {
  const url = `https://${subdomain}.monuments.app`
  const qrBuffer = await QRCode.toBuffer(url, {
    type: 'png',
    width: 400,
    margin: 2,
    color: {
      dark: '#1a1a2e',
      light: '#ffffff'
    }
  })
  // Store buffer to Vercel Blob and return public URL
  // OR return as data URL for download
  return `data:image/png;base64,${qrBuffer.toString('base64')}`
}
  • Generate QR on publish
  • Show QR in dashboard for download (PNG)
  • Optionally embed QR on the monument page footer

Phase 6: Dashboard & User Experience (Days 6-7)

  • Dashboard: view/edit monument details
  • Template preview (live switching)
  • Image reorder/delete
  • QR download button
  • Share link + copy-to-clipboard
  • "Unpublish" button
  • Loading skeletons, error states, empty states

Phase 7: Polish & Production (Day 8+)

  • SEO — dynamic metadata per monument page (generateMetadata)
  • Analytics — Vercel Analytics or Plausible
  • Rate limiting on API routes
  • Image optimization (UploadThing does auto-resize/webp)
  • Custom 404 page for unknown subdomains
  • Proper error boundaries
  • Loading UI (loading.tsx per route)

5. Folder Structure

src/
├── app/
│   ├── layout.tsx              # Root layout with ClerkProvider
│   ├── page.tsx                # Landing page (monuments.app)
│   ├── [subdomain]/
│   │   └── page.tsx            # Dynamic monument landing page (SSR)
│   ├── sign-in/[[...sign-in]]/page.tsx
│   ├── sign-up/[[...sign-up]]/page.tsx
│   ├── dashboard/
│   │   ├── page.tsx            # Dashboard home
│   │   └── preview/
│   │       └── page.tsx        # Live template preview
│   └── api/
│       ├── publish/route.ts
│       ├── check-subdomain/route.ts
│       ├── user/monument/route.ts
│       └── uploadthing/route.ts
├── components/
│   ├── templates/
│   │   ├── TemplateClassic.tsx
│   │   ├── TemplateModern.tsx
│   │   └── TemplateMinimal.tsx
│   ├── OnboardingWizard.tsx
│   ├── ImageUploader.tsx
│   ├── SubdomainPicker.tsx
│   ├── TemplatePicker.tsx
│   └── QRDisplay.tsx
├── lib/
│   ├── prisma.ts               # Prisma client singleton
│   ├── templates.tsx           # Template registry
│   ├── qrcode.ts               # QR generation logic
│   └── uploadthing.ts          # UploadThing config
├── middleware.ts               # Clerk + subdomain routing
├── types/
│   └── index.ts                # Shared TypeScript types
└── styles/
    └── globals.css

6. Deployment Checklist (Vercel)

  • Set environment variables in Vercel dashboard
  • Configure *.monuments.app in Vercel project → Domains
  • Add monuments.app apex domain
  • Run npx prisma migrate deploy on production DB
  • Configure Clerk production URLs (from localhost → monuments.app)
  • Set up UploadThing production env vars
  • Enable Vercel Analytics

7. Future Enhancements (v2)

  • Multi-language — i18n support for monument descriptions
  • Audio guide — embed audio narration per monument
  • Map integration — pin monuments on a city map
  • Social sharing — share to Instagram/TikTok with QR
  • Analytics per monument — visit counter, scan counter
  • Custom domain — allow users to bring their own domain (e.g., eiffeltower.com)
  • Admin panel — moderate content, flagged monuments

8. Key Technical Decisions Summary

Decision Choice Reasoning
Auth Clerk Specified requirement; great DX with Next.js
Subdomain routing Middleware rewrite Clean URLs, no path-based routing needed
Image storage UploadThing Built-in Next.js integration, CDN, auto-optimization
QR generation qrcode npm Pure JS, works server-side, PNG/SVG output
DB ORM Prisma Type-safe, auto-generated types, easy migrations
Templates React Server Components Zero client JS for public pages, fast SSR
Styling Tailwind CSS Rapid prototyping, consistent design system
Hosting Vercel One-command deploy, wildcard domains, Edge middleware