Build a full-stack Content Management System with a React frontend and a Python (Flask) backend, connected via a REST API, using PostgreSQL as the database.
The site has a public-facing homepage showing announcements and news articles, plus an authenticated dashboard for content management with role-based access control.
This document describes the complete CMS target. Delivery is incremental: a milestone may intentionally contain placeholders for requirements assigned to a later milestone. Unless a requirement is explicitly included in a milestone, it remains part of the final deliverable rather than the acceptance scope for that milestone.
Milestone 1 establishes a locally runnable Flask/React project and its integration boundaries. It must make later feature work straightforward, but it does not implement the CMS domain yet.
- Create a Flask backend using an application factory and feature-oriented Blueprint placeholders matching the final backend structure.
- Create a React frontend with Vite, React Router, the final top-level page/component/service folders, and placeholder pages for the public, login, and dashboard routes.
- Make Docker Compose the required Milestone 1 runtime.
docker compose up --buildmust start PostgreSQL, Flask, and Vite together; Flask runs athttp://localhost:5000, Vite athttp://localhost:5173, and PostgreSQL atlocalhost:5432. - Add
GET /api/health, returning JSON such as{"status": "ok"}with HTTP200. - Configure CORS for the frontend origin and add a small frontend API client. The visible home placeholder must call the health endpoint and show a clear connected or unavailable state, proving the browser can reach Flask.
- Add importable placeholder backend modules for auth, articles, announcements, pages, media, categories, tags, and users. Add the corresponding frontend route, page, context, and service modules needed by the final structure. Placeholders need not expose feature CRUD endpoints.
- Wire SQLAlchemy and Flask-Migrate into the Flask application factory and configuration. Initialize and track the Alembic migration environment so future model migrations can be generated consistently.
- Provide
.env.example, root.gitignore, and a rootREADME.mdwith exact Compose startup, shutdown, logs, rebuild, test, frontend build, PostgreSQL, environment, and migration commands. Host-native commands may appear only as optional troubleshooting guidance. - Add service Dockerfiles, health checks, source bind mounts, a container-managed frontend
node_modulesvolume, and a persistent PostgreSQL data volume.
/loginand the unlinked/system-accessroute render an explicit "Login is not available in Milestone 1" state. They must not imply that authentication works or store a fake token.- Every
/dashboardroute redirects to/loginwhile authentication is unavailable. This temporary behavior is replaced by role-aware protected routes in a later milestone. - Any unimplemented backend route below
/api/returns a JSON404response, for example{"error": "Not found"}. It must not return Flask's default HTML error page. - Public content pages may render labeled placeholders until their APIs and models are implemented.
The following requirements remain mandatory for the full CMS, but are deferred to later milestones:
- domain models, relationships, indexes, and the application schema;
- schema-generating migrations for those models and any production data migration;
- signup, login, JWT handling, password hashing, ownership checks, and role authorization;
- working content, taxonomy, media, and user-management CRUD;
- file upload and storage behavior;
- a working seed command and seed data.
Migration tooling is in scope for Milestone 1; domain migration content is not. Alembic configuration and migration files must be committed to version control rather than generated only on a developer's machine. When migrations are later added, the project must verify that flask db upgrade works against the supported PostgreSQL version and that the resulting schema is compatible with the configured SQLAlchemy and database-driver versions.
| Concern | Selected for this project | Alternatives considered | Reason for the skeleton choice |
|---|---|---|---|
| Backend web framework | Flask with Blueprints and an application factory | Django, FastAPI | Matches the full specification while keeping the first milestone small and modular. |
| Frontend build tool | React with Vite | Create React App, a server-rendered framework | Provides a fast development server and a minimal client-only scaffold; Create React App is no longer the preferred new-project baseline. |
| Database access | SQLAlchemy + Flask-Migrate/Alembic | Raw SQL, db.create_all() |
Preserves an explicit migration history and supports the relational domain planned for later milestones. |
| PostgreSQL driver | psycopg |
psycopg2 |
Current driver line with PostgreSQL support; pin a compatible version in backend dependencies. |
| HTTP client | A small wrapper around browser fetch |
Axios | The skeleton needs only base-URL handling and JSON/error normalization, so an extra runtime dependency is unnecessary. |
| Local runtime | Three-service Docker Compose stack with PostgreSQL, Flask, and Vite | Independent host-native processes; serving a built frontend from Flask | Gives Milestone 1 a reproducible one-command runtime while preserving source reload and clear service boundaries. |
Configuration must come from environment variables, with safe local defaults where indicated:
| Variable | Milestone 1 default/example | Purpose |
|---|---|---|
FLASK_APP |
backend.app:create_app |
Flask application factory entry point |
FLASK_DEBUG |
1 for local development |
Enables the local debugger and reload behavior |
SECRET_KEY |
required placeholder in .env.example |
Flask signing secret; no real value is committed |
POSTGRES_DB |
cms |
PostgreSQL database created by Compose |
POSTGRES_USER |
cms |
PostgreSQL user created by Compose |
POSTGRES_PASSWORD |
cms |
Local-only PostgreSQL password example |
DATABASE_URL |
postgresql+psycopg://cms:cms@db:5432/cms |
Compose PostgreSQL connection used by SQLAlchemy and migrations |
FRONTEND_ORIGIN |
http://localhost:5173 |
Exact origin allowed by backend CORS configuration |
VITE_API_BASE_URL |
http://localhost:5000/api |
Frontend API base URL |
The backend must load a root or backend .env file consistently with the README. Environment-specific secrets stay untracked. Vite exposes only variables prefixed with VITE_, so secrets must never use that prefix.
Milestone 1 is complete only when all of the following have been verified from a clean checkout:
docker compose config,docker compose build, anddocker compose up -dsucceed, and all three service health checks pass.GET http://localhost:5000/api/healthreturns HTTP200with exact JSON{"status":"ok"}, and the Vite homepage reports a successful backend health connection.- A request to an unknown
/api/...path returns JSON with HTTP404; a non-API unknown path may use the normal Flask response. /loginand/system-accessstate that login is unavailable, and direct navigation to/dashboardor a nested dashboard URL redirects to/login.- Backend placeholder modules import successfully and the frontend production build completes without unresolved imports.
- Flask-Migrate commands load the application, the tracked Alembic environment is present, and migration status/history can be inspected. Milestone 1 must not fabricate domain tables merely to make a migration non-empty.
- The README Compose commands work with the documented defaults, the PostgreSQL volume persists across a normal stop/start,
.envfiles and generated artifacts remain ignored, and migration files remain tracked. - Dependency versions selected for Flask, SQLAlchemy, Flask-Migrate, Alembic,
psycopg, React, Vite, and React Router are mutually compatible at runtime; verification must include starting both applications, not only successful installation.
| Role | Permissions |
|---|---|
| Visitor (unauthenticated) | View published articles, announcements, and pages only |
| Publisher | + Create, edit, and delete their OWN articles and announcements (as drafts); submit drafts for review; upload and reuse only their own images |
| Admin | + Publish/unpublish any content; edit/delete any user's content; manage categories and tags; manage media library; create/manage Publisher accounts |
| Superadmin | + Manage Admin accounts; full user management (create/edit/delete any user, any role); manage site-wide settings; delete any content unconditionally |
Implement role-checking as a reusable decorator/middleware pattern (e.g., @role_required("admin")) using numeric role levels for comparison, not string matching.
Design and implement database models for:
- User — id, email, password_hash, role (enum: visitor/publisher/admin/superadmin), created_at
- Article — id, title, slug, body, status (draft/pending_review/published), author_id (FK to User), category_id (FK), featured_image_id (FK to Media), created_at, updated_at, published_at
- Announcement — id, title, body, status (draft/published), author_id (FK to User), created_at, published_at, expires_at (optional)
- Page — id, title, slug, body, status (draft/published), author_id (FK to User), updated_at (for static pages like About/Contact)
- Category — id, name, slug
- Tag — id, name, slug (many-to-many with Article)
- Media — id, filename, url, uploaded_by (FK to User), uploaded_at, file_type, source_type, media_type, provider, storage_key, alt_text. Milestone 4 supports locally stored images and external HTTPS links through the same source-aware record. Provider metadata is descriptive only: the backend does not fetch arbitrary URLs or store raw embed HTML. Publisher uploads are private to their uploader and Admins unless referenced by published content; visibility is rechecked for every file request.
Use an ORM (SQLAlchemy) to define these models and their relationships. Include foreign key constraints and appropriate indexes on slug fields.
Structure:
backend/
├── routes/
│ ├── auth.py
│ ├── articles.py
│ ├── announcements.py
│ ├── pages.py
│ ├── media.py
│ ├── categories.py
│ ├── tags.py
│ └── users.py
├── models/
│ ├── user.py
│ ├── article.py
│ ├── announcement.py
│ ├── page.py
│ ├── category.py
│ ├── tag.py
│ └── media.py
├── utils/
│ └── auth_helpers.py
├── app.py
├── config.py
├── requirements.txt
└── .env.example
Auth:
/api/signup— create account (default role: visitor; publisher+ accounts created via admin panel, not public signup)/api/login— email + password for Visitor, Publisher, and Admin roles, returns a JWT/api/superadmin-login— SEPARATE endpoint, only for Superadmin accounts. Must reject login attempts from any non-superadmin user even with correct credentials (checked after password verification). Recommended: rate-limit this endpoint more strictly than/api/login, and log all attempts (success and failure) with timestamp + IP.- Passwords hashed with
werkzeug.security(never store plain text) - JWT includes user id and role; verify and decode on every protected request
login_requireddecorator for any authenticated routerole_required(min_role)decorator for role-gated routes
API endpoints needed (REST conventions, JSON in/out):
- Full CRUD for Articles, Announcements, Pages, Categories, Tags, Media
GETendpoints for public content should only returnstatus=publisheditems when no valid auth token is presentPOST/PUT/DELETEendpoints must check both authentication AND role/ownership (a Publisher can only edit their own drafts; Admin+ can edit anyone's)- Media upload endpoint that accepts file uploads and stores them (local disk storage is fine for now; return a URL)
- External media-link creation for HTTPS URLs, with known-provider classification and no server-side preview fetch. Rich oEmbed/provider adapters, additional upload types, and Markdown rendering remain later extensions of this boundary.
- User management endpoints (
/api/users) restricted to Admin (view/manage Publishers) and Superadmin (view/manage everyone including Admins)
Other requirements:
- Use Flask Blueprints to organize routes by feature
- Use
flask-corsto allow the frontend origin - Use environment variables (via
.env+python-dotenv) forSECRET_KEY, database URL, etc. — provide a.env.examplewith placeholder values - Include a
config.pythat loads these settings - Include basic input validation and proper HTTP status codes (200, 201, 400, 401, 403, 404)
- Use Flask-Migrate (Alembic under the hood) for database migrations. Initialize it during project setup and commit the generated migration environment and subsequent revision files. The README must explain that
flask db initis a one-time maintainer command, documentflask db migratefor creating reviewed revisions, and useflask db upgradein normal fresh-checkout setup. Do not rely ondb.create_all()alone for schema management.
Structure:
frontend/
├── src/
│ ├── components/
│ │ ├── layout/ (Header, Navigation, Footer)
│ │ ├── content/ (ArticleCard, AnnouncementCard, MediaUploader)
│ │ ├── home/ (HomeIntro, AnnouncementSection, ArticleSection)
│ │ └── ui/ (Button, FormField, LoadingSpinner, generic reusable pieces)
│ ├── layouts/ (PublicLayout, AuthLayout, DashboardLayout)
│ ├── pages/
│ │ ├── public/ (Home, ArticlePage, AnnouncementsPage, PageView)
│ │ ├── auth/ (Login, SuperadminLogin)
│ │ └── dashboard/ (DashboardHome, ArticleEditor, AnnouncementEditor,
│ │ ManageUsers, ManageCategories, MediaLibrary)
│ ├── context/
│ │ └── AuthContext.jsx
│ ├── hooks/
│ │ └── useHomeContent.js
│ ├── services/
│ │ ├── api.js (base fetch wrapper, includes auth token automatically)
│ │ ├── articles.js
│ │ ├── announcements.js
│ │ └── auth.js
│ ├── routes/
│ │ └── ProtectedRoute.jsx (role-gated route wrapper)
│ ├── styles.css (global reset and theme-token source)
│ ├── App.jsx
│ └── main.jsx
└── package.json
Homepage (pages/public/Home.jsx):
- Fetches and displays a list of published Announcements (e.g., a banner or top section)
- Fetches and displays a list of published Articles in a newspaper-style layout (headline + summary cards)
- No login required to view
Reusable frontend foundation:
- Establish the reusable frontend foundation before adding dashboard screens. It provides the public, authentication, and dashboard shells and remains the shared presentation boundary for subsequent frontend milestones.
- Route-area shells live in
src/layouts/PublicLayout.jsx,AuthLayout.jsx, andDashboardLayout.jsx. They own the appropriate outer structure and navigation placement; pages compose their content inside the relevant shell rather than duplicating chrome. - Shared site chrome lives in
src/components/layout/Header.jsx,Navigation.jsx, andFooter.jsx. Update these components to change global header, navigation, or footer behavior and content. - Shared controls live in
src/components/ui/, beginning withButton.jsxandFormField.jsx. Reuse these controls for consistent interactive and accessible states; keep page-specific presentation out of the generic controls. - Homepage presentations live in
src/components/home/:HomeIntro.jsx,AnnouncementSection.jsx, andArticleSection.jsx.src/hooks/useHomeContent.jsowns the homepage's coordinated data loading.Home.jsxcomposes those sections and is the single edit point for their display order. src/styles.cssis the source of truth for global reset and theme tokens. Layout, page, and component rules move into co-located CSS Modules and consume those tokens; they must not introduce competing global theme values.- The foundation must preserve React Router, the shared API-client architecture, role-aware protected routes, and the existing public-content behavior while making each layout and component focused on one responsibility.
Auth:
AuthContextstores JWT (in memory + localStorage) and current user's roleservices/api.jsautomatically attaches the token to requests when presentProtectedRoutecomponent wraps dashboard routes and checks the user's role against a required minimum, redirecting unauthorized users
Dashboard (authenticated, role-aware):
- Publisher: sees only their own content in editable lists, with a "New Article" / "New Announcement" button
- Admin: additionally sees ALL users' content, a "Publish/Unpublish" toggle, category/tag management, media library
- Superadmin: additionally sees user management (create/edit/delete Admins and Publishers, change roles)
- Use conditional rendering based on role to show/hide dashboard sections and buttons — don't rely on hiding alone for security, the backend must also enforce it
Site-wide settings (later milestone):
- Superadmins must be able to manage site-wide settings through backend-authorized APIs and a corresponding dashboard experience. The settings model, persistence, validation, audit/authorization behavior, and public application of settings remain outstanding after the reusable frontend foundation and Milestone 6.
Routing:
- Use React Router
- Public routes:
/,/articles/:slug,/announcements,/pages/:slug,/login - Superadmin login: a separate, unlinked route (e.g.,
/system-accessor similar non-obvious path — not linked from any nav/UI, must be typed/bookmarked directly). This page calls/api/superadmin-logininstead of/api/login, and should look visually distinct from the regular login page so it's clear which account type it's for - Dashboard routes:
/dashboard,/dashboard/articles/new,/dashboard/articles/:id/edit,/dashboard/users(admin+), etc. - After a successful superadmin login, redirect into the same
/dashboard, since permissions are still enforced by role on every route and API call — the separate login page is about isolating the entry point, not creating a separate app
- Include a root
README.mdexplaining: project structure, how to run backend (pip install -r requirements.txt, set up.env, run migrations,flask run), how to run frontend (npm install,npm run dev), and how the two connect - Include a
.gitignorefor both frontend and backend (node_modules, pycache, .env, venv, build artifacts) - Backend should include a simple database seed script that creates one Superadmin account and a few sample categories, so the app is usable immediately after setup. The Superadmin's initial email and password must be read from environment variables (e.g.,
SEED_SUPERADMIN_EMAIL,SEED_SUPERADMIN_PASSWORDin.env.examplewith placeholder values) — never hardcode real or example credentials directly in the seed script - Keep components and route handlers small and single-purpose — one file, one responsibility
- Add brief comments explaining non-obvious logic, especially around role-checking and JWT handling
The final deliverable is the complete CMS described in Sections 1–5: a working Flask backend with all models, routes, authentication, and role logic; a working React frontend with public and dashboard experiences, auth context, and protected routes; and a README with setup instructions. The completed code must run locally with a PostgreSQL database after following the README steps.
Milestone 1 is the first incremental delivery toward that target and is accepted only against the scope and checks in “Milestone 1: Runnable Project Skeleton.” Its explicit placeholders and deferrals do not remove or weaken any final-deliverable requirement. Later milestones must replace the placeholder login, dashboard redirects, public content, and API behavior with the corresponding complete functionality in Sections 1–5.