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.

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

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