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
2. CMS Choice
Three commercially available CMS products were evaluated against XAMXAM's requirements.
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
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)
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
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 currentmedia.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/:slugstudent 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
- Public catalogue: browse, paginate, filter by year / orientation / keyword
- Full-text search across title, author, synopsis, keywords
- TFE detail page: all metadata, files (gated by access type), jury composition
- Three file access tiers: open, restricted (cookie token), forbidden
- Admin: full thesis lifecycle (draft → published → archived)
- Admin: manage all lookup tables (orientations, formats, languages, licences…)
- Admin: share-link CRUD (create, toggle, set password, set expiry, archive)
- Admin: audit log of all content changes
- Admin: CSV export and full database/file archive download
- Admin: configurable SMTP relay with encrypted credential storage
- Admin: configurable PeerTube integration (instance URL, credentials, channel, privacy)
- Admin: inline form help text, configurable per field
- Student portal: access via share link, optional password gate
- Student portal: multi-file upload queue with reorder and removal
- Student portal: full thesis submission form with inline validation
- Student portal: email confirmation on successful submission
- Maintenance mode toggle (admin-accessible while public site is blocked)
- OG / Twitter Card meta tags on all public pages
- WCAG 2.1 AA accessibility on public pages and student form
Non-functional requirements
- Security: no uploaded file is ever directly web-accessible; all served via authenticated proxy
- Security: SMTP and PeerTube passwords encrypted at rest (AES-256)
- Security: CSRF protection on all forms
- Security: rate limiting on public search and share-link validation endpoints
- Performance: cover images batch-loaded; ISR (Next.js Incremental Static Regen) for public pages
- Deployment: single VPS, two processes (Strapi + Next.js) behind nginx
- Database: SQLite for parity with current setup; PostgreSQL upgrade path must be supported
- Backups: existing
just deploy-db+ rsync workflow preserved or equivalent provided - Internationalisation: UI in French; content supports multilingual entries (fr, nl, en)
- 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.
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
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
Restricted Access + File Access Tokens
1 week- Strapi
file-accessplugin: custom routePOST /api/file-access/request - Token generation, storage in
file_access_tokenstable, 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
Student Submission Portal
4 weeks- Strapi
share-linkplugin: 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
ThesisCreateControllerlogic - 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
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
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
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 |
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).
1000 Bruxelles
TVA: BE 0123.456.789
jane@example.dev
É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
.sqlfiles - 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
htpasswdmanagement - 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)
- LDAP integration — documented in
LDAP_SPEC.mdbut 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
- 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.sqlpending) — resolve pending migrations before migrating data - Legacy artefacts in
oui/nonfields (025_fix_oui_non_artefacts.sqlpending) — same - Encrypted SMTP password — must be decrypted during migration (requires the current
Crypto.phpkey) and re-encrypted by Strapi lifecycle hook