# XAMXAM — CMS Migration Plan

## 1. Project Overview

XAMXAM (formerly Posterg) is the thesis/TFE (Travail de Fin d'Études) repository for the ERG — École de Recherche Graphique in Brussels. It is a purpose-built PHP 8.4 + SQLite3 application deployed behind nginx.

### Current tech stack

- PHP 8.4, SQLite 3, nginx
- Custom MVC (no framework)
- HTMX for partial rendering
- No npm / no build step
- rsync deployment (no git on server)
- Single-server, single-file database

### What the system does

- Public browseable + searchable thesis catalogue
- Admin panel: create / edit / publish / export theses
- Student submission via one-time share links
- Controlled file serving (PDFs, images, video, audio)
- PeerTube video upload integration
- SMTP email notifications with encrypted credentials
- Audit log, maintenance mode, CSV/DB export
- Access control: open / restricted (cookie token) / forbidden

Scope of this plan
The goal is a feature-equivalent rebuild using an off-the-shelf CMS — meaning the ERG team would no longer need to maintain bespoke PHP controllers, database migrations, or a custom admin panel. Custom code is minimised to integrations that no CMS ships out-of-the-box (share links, restricted file access, PeerTube upload).

## 2. CMS Choice

Three commercially available CMS products were evaluated against XAMXAM's requirements.

Recommended

### Strapi v5

Headless Node.js CMS — self-hosted, open-source core

#### Pros

- Rich relation fields (authors, jury, files, tags) built-in
- Admin panel generated from content-type schema
- Role-based access control (admin vs. student roles)
- Media library with upload providers
- REST + GraphQL API out-of-the-box
- Lifecycle hooks for custom logic
- Plugin system for custom routes
- SQLite supported (small team, single server)

#### Cons

- JavaScript/Node.js — different from current PHP stack
- Separate frontend required (Next.js or similar)
- Share-link submission portal = custom plugin
- File-access tokens = custom plugin
- PeerTube upload = custom plugin
- More moving parts than current single-PHP-server setup

Alternative

### Kirby CMS 4

Flat-file PHP CMS — familiar language, no database required

#### Pros

- PHP — same language as today
- Flexible content blueprints
- Custom routes and hooks
- Panel (admin UI) with custom sections
- Lightweight, no extra runtime

#### Cons

- Flat-file (YAML/Markdown) — poor fit for relational thesis data
- No built-in user roles for student submission
- Search, pagination, and filtering need custom code
- Virtually all the complex logic still needs writing
- Comercial licence required ($199/site)

Not suitable

### WordPress

PHP CMS + plugin ecosystem

#### Pros

- Familiar to many users
- Large plugin ecosystem

#### Cons

- Content model too blog-centric for thesis metadata
- Custom post types + ACF = heavyweight approach
- Student submission portal not in any standard plugin
- Security attack surface is substantial
- Performance tuning overhead for file serving

Verdict
Strapi v5 is the best match. Its content-type system maps naturally to the thesis data model (relations, media, nested metadata). The admin panel is generated automatically from the schema. Custom plugins cover the three features not provided by any CMS out-of-the-box: share links, restricted file access, and PeerTube upload.

## 3. Proposed Architecture

      ┌──────────────────────────────────────────────────────────────────────┐
      │                          nginx (reverse proxy)                        │
      │        /          → Next.js (port 3000)                               │
      │        /admin      → Strapi admin UI (port 1337/admin)                │
      │        /api        → Strapi REST API (port 1337/api)                  │
      │        /partage/:slug → Next.js (student submission route)            │
      └──────────────────────────────────────────────────────────────────────┘
              │                         │
              ▼                         ▼
      ┌──────────────────┐    ┌──────────────────────────────────────────┐
      │   Next.js 15      │    │           Strapi v5 (Node.js)            │
      │  (public site +   │    │                                          │
      │  student portal)  │◄──►│  Content Types: Thesis, Author,          │
      │                   │    │  Supervisor, Tag, Format, Language,      │
      │  - Home page      │    │  ShareLink, AccessToken, SiteSettings    │
      │  - TFE detail     │    │                                          │
      │  - Search         │    │  Plugins:                                │
      │  - /partage form  │    │   • share-link  (custom routes + logic)  │
      │  - File proxy     │    │   • file-access (token + cookie auth)    │
      └──────────────────┘    │   • peertube    (upload integration)     │
                               │                                          │
                               │  Media: local upload provider            │
                               │  Database: SQLite (or PostgreSQL)        │
                               └──────────────────────────────────────────┘
                                             │
                                             ▼
                               ┌──────────────────────────┐
                               │  Storage (outside webroot) │
                               │  /var/www/xamxam/uploads/  │
                               │  (TFE PDFs, covers,        │
                               │   annexes, video, audio)   │
                               └──────────────────────────┘

### Key decisions

- SQLite is kept — Strapi supports it natively. Migration to PostgreSQL is a one-line config change if needed later.
- Files are never directly web-accessible — Next.js `/api/media/[...path]` proxies file requests, enforcing access-type checks (same as current `media.php`).
- Strapi admin panel replaces the custom PHP admin — thesis CRUD, jury management, share-link management, settings, and audit log all live there.
- Next.js App Router handles the public site and the `/partage/:slug` student submission portal.
- Single server deployment — both processes (`strapi start`, `next start`) run behind nginx, same as current setup.

## 4. Feature Map

Tags: native = ships out-of-the-box with the CMS, plugin = available via an existing Strapi plugin or npm package, custom = needs bespoke code, drop = intentionally not reproduced (replaced or unnecessary).

Feature (current XAMXAM) | CMS equivalent | Coverage | Notes
--- | --- | --- | ---
Public site
Home page — latest year, random order, year filter | Next.js page fetching Strapi `/api/theses` | native | Strapi REST supports sort, filter, populate. Random order via `RANDOM()` is a custom sort plugin or a shuffle in Next.js.
Paginated browse (24/page) | Strapi pagination params (`pagination[page]`) | native |
Full-text search (title, author, keyword, year, orientation) | Strapi filters + Next.js search page | native | Strapi's `$containsi` operator covers basic search. For true FTS, add the `strapi-plugin-fuzzy-search` package.
Rate limiting on search | nginx `limit_req_zone` or Strapi middleware | plugin | `koa-ratelimit` as a Strapi middleware.
TFE detail page with all metadata | Next.js dynamic route `/tfe/[id]` | native | Use `populate[]=authors&populate[]=jury&populate[]=files` etc.
OG / Twitter Card meta tags | Next.js `generateMetadata()` | native |
Email obfuscation for author contact | Client-side JS decode or Next.js server component | custom | ~10 lines of logic. No CMS ships this.
Maintenance mode | Next.js middleware redirect + flag env var | custom | Simple; `MAINTENANCE=1` → middleware returns 503 page.
File serving & access control
Controlled file proxy (PDFs, images, video, audio) | Next.js API route `/api/media/[...path]` | custom | Replicates current `media.php`. Streams file, validates MIME, enforces access type.
Access types: open / restricted / forbidden | Strapi `access_type` relation + media proxy | custom | Strapi stores the type; the proxy enforces it.
Cookie-based restricted access tokens | Custom `file-access` Strapi plugin | custom | Generates signed tokens, stores in DB, sets `HttpOnly` cookies. Same logic as current `FileAccessController.php`.
File labels, sort order per thesis | Strapi relation with pivot fields | native | Strapi allows custom fields on many-to-many pivot tables (v5 "dynamic zones" or custom join table).
Admin panel
Thesis CRUD (create / edit / delete) | Strapi admin content manager | native | All fields configured in content-type schema. No PHP to write.
Publish / unpublish thesis | Strapi draft & publish system | native | Toggle on the content entry. Matches current `is_published` flag.
Author management (name, email, contact visibility) | Strapi `Author` content type with relation | native | Authors are a separate content type, reusable across theses.
Jury management (role, interne/externe/ULB) | Strapi `Supervisor` type + pivot role field | native | Role dropdown (president / promoteur / lecteur) on the relation. ULB flag as boolean.
File upload queue with drag-reorder | Strapi Media Library + custom admin plugin UI | custom | Strapi's built-in media library handles upload. Drag-reorder with a custom "sort_order" component needs a small admin UI plugin (~1–2 days).
PeerTube video/audio upload | Custom `peertube` Strapi plugin | custom | Replicates `PeerTubeService.php`. OAuth2 password grant, credential encryption, token refresh on 401. ~3–5 days.
Share-link management (create, toggle, archive) | Custom `share-link` Strapi plugin | custom | Custom content type + admin panel list view. Slug generation, optional password hash, expiry, usage count. ~3–4 days.
CSV export | Custom Strapi controller or admin plugin action | custom | ~1 day. Strapi lifecycle hooks make CSV generation straightforward.
Database / file archive export | Custom Strapi controller (zip SQLite + storage dir) | custom | ~1 day. Identical logic to current `ExportController.php`.
Audit log | Strapi `strapi-plugin-audit-log` or custom | plugin | Community plugin `strapi-audit-log` covers create/update/delete. Custom entries for share-link events.
Admin login (htpasswd + PHP session) | Strapi native JWT-based admin auth | native | nginx `auth_basic` layer can remain as a second factor if desired.
Site settings (SMTP, PeerTube, help text) | Strapi Single-Type `SiteSettings` | native | Single-Type entries allow one-off config records. Encrypted fields via Strapi lifecycle hooks.
SMTP send with encrypted password | Custom Strapi service + `nodemailer` | plugin | `@strapi/provider-email-nodemailer` for delivery. AES encryption of the stored password via a lifecycle hook.
Inline form help text (configurable per field) | Strapi Single-Type `FormHelp` + admin UI | native | Store help blocks as a Single-Type with one entry per field. Renders in the Next.js form.
Import from CSV / legacy data | Custom Strapi import script (run once) | custom | One-time migration. ~3–5 days depending on data quality.
Student submission portal (/partage)
Share-link access (slug + optional password) | Next.js `/partage/[slug]` + Strapi `/api/share-links/:slug/validate` | custom | Custom Strapi route validates slug, checks password, expiry, usage count. Issues a JWT session for the student.
Student TFE submission form | Next.js form + Strapi `/api/share-links/:slug/submit` | custom | Replicates the full `/partage/index.php` form. All validation server-side in Strapi. ~4–6 days.
Incremental file upload queue with reorder | Next.js client-side queue + Strapi upload endpoint | custom | Client-side JS queue (as planned in `TODO.md`). Submits FormData on final form submit. Same plan as the current in-progress refactor.
Email confirmation on submission | Strapi lifecycle hook → nodemailer | plugin | On thesis creation event, fire email via the nodemailer provider.
Summary / recap page | Next.js `/partage/[slug]/recap` | custom | Renders after successful submission. ~0.5 days.
Lookup data & taxonomy
Orientations, AP programs, finality types, languages, formats, license types | Strapi content types (or Enumeration fields) | native | All managed via Strapi admin. Relations from the Thesis type.
Tags / keywords (free-text, multi-value) | Strapi `Tag` content type with many-to-many | native | Autocomplete in admin via Strapi's default relation field.

## 5. Requirements

### Functional requirements

1. Public catalogue: browse, paginate, filter by year / orientation / keyword
2. Full-text search across title, author, synopsis, keywords
3. TFE detail page: all metadata, files (gated by access type), jury composition
4. Three file access tiers: open, restricted (cookie token), forbidden
5. Admin: full thesis lifecycle (draft → published → archived)
6. Admin: manage all lookup tables (orientations, formats, languages, licences…)
7. Admin: share-link CRUD (create, toggle, set password, set expiry, archive)
8. Admin: audit log of all content changes
9. Admin: CSV export and full database/file archive download
10. Admin: configurable SMTP relay with encrypted credential storage
11. Admin: configurable PeerTube integration (instance URL, credentials, channel, privacy)
12. Admin: inline form help text, configurable per field
13. Student portal: access via share link, optional password gate
14. Student portal: multi-file upload queue with reorder and removal
15. Student portal: full thesis submission form with inline validation
16. Student portal: email confirmation on successful submission
17. Maintenance mode toggle (admin-accessible while public site is blocked)
18. OG / Twitter Card meta tags on all public pages
19. WCAG 2.1 AA accessibility on public pages and student form

### Non-functional requirements

1. Security: no uploaded file is ever directly web-accessible; all served via authenticated proxy
2. Security: SMTP and PeerTube passwords encrypted at rest (AES-256)
3. Security: CSRF protection on all forms
4. Security: rate limiting on public search and share-link validation endpoints
5. Performance: cover images batch-loaded; ISR (Next.js Incremental Static Regen) for public pages
6. Deployment: single VPS, two processes (Strapi + Next.js) behind nginx
7. Database: SQLite for parity with current setup; PostgreSQL upgrade path must be supported
8. Backups: existing `just deploy-db` + rsync workflow preserved or equivalent provided
9. Internationalisation: UI in French; content supports multilingual entries (fr, nl, en)
10. Maintainability: no custom admin UI beyond what Strapi's plugin API supports; avoid forks of the CMS core

### Dependencies & integrations

- PeerTube instance (existing, ERG-hosted)
- SMTP relay (existing ERG mail server)
- nginx (existing, config updated for new port routing)
- Existing SQLite database (migrated via one-time import script)
- Existing file storage directory (copied to new upload path)

## 6. Delivery Timeline

Estimated at 1 developer, full-time. Reduce scope or add a second dev to compress.

Phase 1

### Foundation & Data Model

3 weeks

- Initialise Strapi v5 project with SQLite
- Define all content types: `Thesis`, `Author`, `Supervisor`, `ThesisFile`, `Tag`, `Orientation`, `ApProgram`, `FinalityType`, `Language`, `FormatType`, `LicenseType`, `AccessType`, `SiteSettings`, `FormHelp`
- Configure relations and pivot fields (jury role, author order, file sort_order, file label)
- Enable Draft & Publish on `Thesis`
- Write data migration script: read SQLite → POST to Strapi API
- Migrate all files to Strapi upload directory
- Validate migrated data in admin panel
- Deliverable: Strapi running locally with full existing dataset

Phase 2

### Public Site (Next.js)

4 weeks

- Initialise Next.js 15 App Router project
- Home page: latest year random grid + year filter + pagination
- Search page: full-text across all fields, filter controls
- TFE detail page: all metadata sections, jury composition, file list
- File proxy route `/api/media/[...path]` with access-type enforcement
- OG / Twitter meta tags via `generateMetadata()`
- CSS port from current design (reuse existing `public.css`, `tfe.css`)
- Email obfuscation component
- Maintenance mode middleware
- Deliverable: public site feature-complete, pixel-comparable to current

Phase 3

### Restricted Access + File Access Tokens

1 week

- Strapi `file-access` plugin: custom route `POST /api/file-access/request`
- Token generation, storage in `file_access_tokens` table, cookie issuance
- File proxy reads cookie, validates token against DB
- Access request form in Next.js TFE detail page
- Admin panel: view / revoke tokens per thesis
- Deliverable: restricted file access parity with current

Phase 4

### Student Submission Portal

4 weeks

- Strapi `share-link` plugin: content type + slug generation + validate/submit routes
- Next.js `/partage/[slug]`: slug gate, optional password form
- Student submission form: all fields (title, subtitle, authors, jury, synopsis, keywords, formats, languages, licence, files)
- Client-side file upload queue (TFE, annexes, cover, video, audio) with drag-reorder, MIME/size validation
- Server-side validation replicating current `ThesisCreateController` logic
- Email confirmation via nodemailer on submission
- Recap / summary page
- Admin panel: share-link list with create / toggle / archive / set-password actions
- Deliverable: student portal fully functional end-to-end

Phase 5

### Admin Completions

3 weeks

- PeerTube plugin: settings form, OAuth2 token management, upload action on thesis edit
- CSV export endpoint
- Database + files archive download endpoint
- Audit log plugin (or configure `strapi-audit-log`)
- Admin file-reorder UI plugin (drag handles on thesis media list)
- SMTP settings + test-send + encrypted storage via lifecycle hook
- FormHelp single-type admin UI + rendering in student form
- Deliverable: all admin features present, team can self-manage

Phase 6

### QA, Hardening & Cutover

3 weeks

- Security review: headers, rate limiting, CSRF, upload validation, path traversal
- WCAG 2.1 AA audit on public pages and student form
- Performance: ISR on TFE pages, cover image optimisation (Next.js `Image`)
- nginx config update for new port routing
- Staging deploy: run both old and new in parallel; team acceptance testing
- Production cutover: deploy new stack, migrate DB + files, update DNS if needed
- Post-cutover monitoring (1 week watch period)
- Deliverable: production running on new stack

Total: 18 weeks (approx. 4.5 months) at 1 full-time developer. Add 4–6 weeks buffer for stakeholder review cycles, scope additions, or part-time availability. Realistic end-to-end: 22–24 weeks from kickoff to production.

## 7. Effort Breakdown

Area | Est. days | Relative effort | Main tasks
--- | --- | --- | ---
Data model & migration | 10 |  | Schema definition, migration script, data validation
Public site (Next.js) | 15 |  | All public pages, file proxy, metadata, CSS port
File access control plugin | 5 |  | Token generation, cookie auth, admin revoke view
Student submission portal | 18 |  | Share-link plugin, student form, file queue, email confirm
Admin completions | 14 |  | PeerTube plugin, exports, audit log, SMTP, file reorder UI
QA, security & cutover | 13 |  | Security review, WCAG, performance, staging, production deploy
Total | 75 days (~15 weeks) |  | + 3–4 weeks buffer = 18–19 weeks

Cost note
Strapi Community Edition is free / open-source. Strapi Cloud (hosted) starts at $29/month. For self-hosted (current VPS setup), no licence fee applies. Next.js is MIT. The only paid dependency is a Vercel deployment (optional — self-hosted Node.js is free). Total additional infrastructure cost: $0/month on existing VPS.

## 8. Invoice Example — Junior Solo Developer

The figures below illustrate what a realistic invoice from a junior freelance developer might look like for this project. Assumptions: €350/day (junior solo rate, Western Europe), 75 billable days of work spread across 18–19 weeks, with a 10% contingency buffer added as a separate line item. VAT at 21% (Belgian standard rate, adjust for your jurisdiction).

Dev Freelance — Jane Doe Rue de l'Exemple 12
1000 Bruxelles
TVA: BE 0123.456.789
jane@example.dev
FACTURE / INVOICE N° 2026-042
Date: 10 mai 2026
Échéance: 10 juin 2026
Réf client: ERG / XAMXAM
Facturé à / Billed to

École de Recherche Graphique (ERG)
Rue du Page 87, 1050 Bruxelles
TVA: BE 0400.000.000

Description | Jours | Taux jour | Total HT
--- | --- | --- | ---
Phase 1 — Data model & migration
Définition des content-types Strapi (Thesis, Author, Supervisor, Tag, etc.) + relations et champs pivot | 5 | 350 € | 1 750 €
Script de migration SQLite → Strapi API + transfert des fichiers, validation des données | 5 | 350 € | 1 750 €
Phase 2 — Site public (Next.js)
Page d'accueil, filtres par année, pagination (24/page) | 3 | 350 € | 1 050 €
Page de recherche (filtres titre / auteur / mot-clé / année / orientation) | 3 | 350 € | 1 050 €
Page de détail TFE (métadonnées, jury, fichiers, OG tags) | 4 | 350 € | 1 400 €
Proxy de fichiers `/api/media/[…path]` + contrôle d'accès + port CSS | 5 | 350 € | 1 750 €
Phase 3 — Accès restreint aux fichiers
Plugin Strapi `file-access` (tokens, cookies, révocation admin) | 5 | 350 € | 1 750 €
Phase 4 — Portail étudiant (/partage)
Plugin Strapi `share-link` (génération slug, validation, routes submit) | 5 | 350 € | 1 750 €
Formulaire de dépôt étudiant (Next.js) — tous les champs, validation serveur | 6 | 350 € | 2 100 €
File upload queue côté client (tri, suppression, validation MIME/taille) | 4 | 350 € | 1 400 €
Email de confirmation (nodemailer) + page de récapitulatif | 3 | 350 € | 1 050 €
Phase 5 — Completions admin
Plugin PeerTube (OAuth2, upload vidéo/audio, chiffrement credentials) | 4 | 350 € | 1 400 €
Export CSV + archive DB/fichiers | 2 | 350 € | 700 €
Audit log, SMTP chiffré, FormHelp, UI tri fichiers | 4 | 350 € | 1 400 €
Gestion share-links dans le panel admin (liste, toggle, archive, mot de passe) | 4 | 350 € | 1 400 €
Phase 6 — QA, sécurité & mise en production
Audit sécurité (headers, rate-limiting, CSRF, upload, path traversal) | 3 | 350 € | 1 050 €
Audit accessibilité WCAG 2.1 AA (site public + formulaire étudiant) | 2 | 350 € | 700 €
Configuration nginx, déploiement staging, tests d'acceptation, mise en production | 4 | 350 € | 1 400 €
Monitoring post-lancement (1 semaine), corrections mineures incluses | 4 | 350 € | 1 400 €
Réserve
Contingence 10 % — imprévus, retours client, ajustements de scope | 7.5 | 350 € | 2 625 €

Sous-total HT (75 jours × 350 €) | 26 250 €
Contingence 10 % (7.5 jours × 350 €) | 2 625 €
Total HT | 28 875 €
TVA 21 % | 6 063.75 €
TOTAL TTC | 34 938.75 €

## 9. Trade-offs vs. Current Custom Build

### What you gain

- No bespoke admin panel maintenance — Strapi generates the CRUD UI from schema; adding a field to a thesis takes 2 minutes in the GUI, not 30 lines of PHP
- Schema changes are safe — Strapi manages migrations automatically; no hand-written `.sql` files
- REST + GraphQL API out-of-the-box — a future mobile app, integration, or external catalogue can consume the API immediately
- Media Library UI — upload, rename, and organise files through a proper UI with thumbnails
- Role-based access control built-in — adding a new admin user takes seconds; no `htpasswd` management
- Community plugins for audit log, search, email — maintained by others
- Upgrade path to PostgreSQL — one config line; no schema rewrite
- TypeScript end-to-end — type safety from API response to rendered component

### What you lose / accept

- Operational simplicity — two processes (Strapi + Next.js) instead of one PHP server; slightly more to monitor and deploy
- PHP familiarity — custom plugins are JavaScript/TypeScript; the team needs to learn (or hire for) Node.js
- Smaller codebase for simple tasks — a 20-line PHP controller becomes a Strapi plugin with boilerplate. The abstraction is a net win at scale, but adds friction for tiny changes
- Strapi CMS upgrades — major version upgrades (v4→v5 was breaking) require effort; the team is now on a CMS release cadence
- Node.js on the server — higher baseline memory (~150MB for Strapi) vs. PHP's per-request model (~20MB)
- HTMX hypermedia patterns lost — replaced by conventional React client-side interactivity; not a functional loss, but a philosophical one for the current codebase's author

## 10. What Is Not Included (Out of Scope)

Intentionally deferred or dropped

- LDAP integration — documented in `LDAP_SPEC.md` but not currently implemented; deferred to a post-launch phase. Strapi's Users & Permissions plugin supports LDAP via a third-party provider when ready.
- Redesign — this plan reproduces the current design and UX faithfully. No visual redesign is included.
- Multi-tenancy / multi-school — not a current requirement. Strapi supports this but it would be a separate project.
- Full-text search indexing (Elasticsearch / Meilisearch) — Strapi's built-in filter-based search is sufficient at ERG's dataset size (~hundreds of theses). Meilisearch integration can be added as a plugin later if needed.
- Video player / streaming — current app embeds PeerTube's existing player via URL; that behaviour is preserved. No custom video player is built.
- Strapi Cloud deployment — plan assumes self-hosted on the existing ERG VPS. Cloud hosting is a drop-in option at any point.

### Migration data quality risks

Data migration risks to verify before Phase 1

- SQLite views (`v_theses_full`, `v_theses_public`) aggregate denormalised strings (comma-separated authors etc.) — the migration script must split these back into proper relations
- Banner/cover path consolidation (`027_drop_banner_path.sql` pending) — resolve pending migrations before migrating data
- Legacy artefacts in `oui/non` fields (`025_fix_oui_non_artefacts.sql` pending) — same
- Encrypted SMTP password — must be decrypted during migration (requires the current `Crypto.php` key) and re-encrypted by Strapi lifecycle hook