NodePress CMS
A self-hosted headless CMS. Define your content structure, fill it with data through the admin panel, then consume it via a clean REST API from any website, app, or platform.
Installation — Step by Step
Follow these steps in order. Each step takes only a few minutes. No prior coding experience required.
Already have some tools installed?
Run these commands in your terminal to check. If you see a version number, you can skip that step.
Install Node.js
node -v — if it shows v18 or higher, skip to Step 2.
Node.js is the engine that runs NodePress. Download and install version 18 or newer.
Download Node.js from nodejs.orgAfter installing, verify: node -v should print something like v22.0.0
Install Git
git --version — if it shows a version number, skip to Step 3.
Git is used to download the NodePress source code. You only need to install it — no need to learn how to use it.
Download Git from git-scm.comOn Windows: click Next through all the options — the defaults are fine.
Install PostgreSQL
postgres user — you'll need it in Step 5. Then skip to Step 4.
PostgreSQL is the database where all your content is stored. Think of it as the filing cabinet behind the scenes.
Download PostgreSQL from postgresql.org⚠ Important during installation:
- When asked to set a password for the postgres user, write it down — you will need it in Step 5.
- Leave the port as 5432 (the default).
- PostgreSQL will run automatically in the background after installation.
Create your NodePress project
Open a terminal, navigate to the folder where you want your project, and run:
Replace my-cms with your project name. This downloads NodePress, generates secret keys, and installs all dependencies. Takes 2–5 minutes.
Connect NodePress to your database
The CLI generates a random database password, but NodePress needs to connect to your PostgreSQL using the password you set in Step 3.
Open my-cms/backend/.env in any text editor (Notepad is fine) and find this line:
Replace RANDOM_PASSWORD with the password you set when installing PostgreSQL:
What is DATABASE_URL?
It's the address NodePress uses to find and log into your database. postgres is the username, the part after : is your password, localhost:5432 is where the database lives on your computer, and nodepress is the database name that will be created automatically.
DATABASE_URL="postgresql://postgres@localhost:5432/nodepress"
Create the database tables
This command sets up all the tables NodePress needs. You only run this once.
Start the backend
The backend API is now running at http://localhost:3000. Keep this terminal open.
Start the admin panel
Open a new terminal window (keep the backend one running) and run:
Admin panel is now at http://localhost:5173. Keep this terminal open too.
Create your admin account
Open your browser and go to http://localhost:5173. You will be taken to the setup page automatically. Enter your site name, email, and a password.
🎉 You're done!
NodePress is running. You can now create content types, add entries, upload media, and start using the API. The setup page only appears once — it's disabled permanently after the first account is created.
Quick Start
Create a content type
Go to Content Types → New. Give it a name like blog and add fields: title (text), body (richtext), published (boolean).
Add an entry
Go to Entries → blog → New Entry. Fill in the fields. A URL-friendly slug is auto-generated from the title.
Fetch via API
Content Types
Content types define the shape of your data. Each content type has a name and a schema — a list of fields with types and options. Think of them as database tables with a visual builder.
Naming
Names are stored lowercased and snake_cased. Blog Posts → blog_posts
Schema
Each field has a name, type, label, and optional settings like required, options list, or sub-fields.
API
Creating a type instantly generates GET /api/{type} and GET /api/{type}/{slug}.
auth, media, entries, content-types, uploads — these are blocked to avoid route conflicts.
Field Types
| Type | Description | JSON value |
|---|---|---|
| text | Short single-line text. Good for titles, names, labels. | "My Blog Post" |
| textarea | Multi-line plain text. Good for short descriptions. | "A short summary..." |
| richtext | HTML from WYSIWYG editor. Supports headings, images, links. | "<p>Hello</p>" |
| number | Integer or decimal number. | 42 |
| boolean | True/false toggle. Good for published, featured flags. | true |
| select | One value from a predefined list of choices. | "tech" |
| image | A URL string pointing to an image (from Media Library or external). | "/uploads/photo.jpg" |
| repeater | A list of items, each sharing the same sub-fields. | [{"name":"Kartik"}] |
| flexible | A list of blocks where each block can be a different layout. | [{"_layout":"hero"}] |
Entries & Slugs
Entries are the data records for a content type. Each entry has a slug, a status, and a data object containing all field values.
Slugs
Auto-generated from the first text field. Locked after creation. Must be unique per content type.
Status
published entries are public. draft entries are hidden from the public API.
Scheduling
Set a publishAt date to automatically publish an entry in the future.
Versions
Every save creates a version snapshot. Restore any previous version from the entry editor.
Soft delete
Deleted entries are soft-deleted (hidden, not removed). Restore from the admin panel if needed.
SEO
Each entry has optional SEO fields: title, description, image, and noIndex toggle.
Media Library
Upload and manage files through the admin panel. Images are automatically optimised and converted to WebP.
Allowed types
Limits
Max file size: 10MB. Images are resized to a max of 2400px and converted to WebP automatically.
backend/uploads/ by default. Set STORAGE_DRIVER=s3 to use S3, Cloudflare R2, or any S3-compatible service.
API Keys
API keys let external apps read or write content without a user login. Send the key in the X-API-Key header.
| Access level | Can do | Rate limit |
|---|---|---|
| read | GET requests only | 120 req/min |
| write | POST / PUT / PATCH / DELETE | 60 req/min |
| all | Read + Write combined | 120 req/min |
Forms
Build forms in the admin panel and embed them in your frontend. Submissions are stored in the database and can trigger email or webhook actions.
Form field types: text, email, textarea, number, select, radio, checkbox
Webhooks
Webhooks fire HTTP requests to external URLs when content events happen. Useful for triggering rebuilds, sending notifications, or syncing with other services.
Events
entry.created
entry.updated
entry.deleted
media.uploaded
* (all events)
Retry logic
Failed deliveries are retried with exponential backoff. Up to 5 attempts over ~30 minutes. HMAC-SHA256 signature in X-NodePress-Signature header.
SEO & Sitemap
Sitemap
Auto-generated at GET /api/sitemap.xml. Includes all published entries. Set SITE_URL in your env.
Robots.txt
Served at GET /api/robots.txt. Configure blocked paths via ROBOTS_DISALLOW env var.
Self-Hosting
Environment variables
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string required |
JWT_SECRET | 64+ char random secret for auth tokens required |
CORS_ORIGIN | Allowed frontend origin (comma-separated for multiple) required |
PORT | API port (default 3000) |
APP_URL | Backend URL — used in API responses |
SITE_URL | Public site URL — used in sitemap.xml |
REDIS_URL | Redis URL — enables shared cache (optional) |
STORAGE_DRIVER | local (default) or s3 |
STORAGE_S3_BUCKET | S3/R2/MinIO bucket name (if STORAGE_DRIVER=s3) |
SMTP_HOST | SMTP server for password reset emails |
METRICS_TOKEN | Bearer token to protect GET /api/metrics |
Docker (production)
API Reference
All endpoints are prefixed with /api. Public GET endpoints require no auth. Write endpoints require Authorization: Bearer <token> or X-API-Key.
Auth
/api/auth/loginEmail + password → returns JWT access token
/api/auth/meReturns current user from token
/api/auth/refreshExchange refresh token for new access token
Content (Public)
/api/:typeList all published entries for a content type. Supports ?page, ?limit, ?status
/api/:type/:slugGet a single published entry by slug
/api/:typeCreate a new entry
/api/:type/:slugUpdate an entry
/api/:type/:slugSoft-delete an entry
Media
/api/mediaList all uploaded files
/api/media/uploadUpload a file (multipart/form-data, field: file)
/api/media/:idDelete a file by ID
Other
/api/submit/:slugSubmit a form (no auth required)
/api/healthHealth check — DB connectivity
/api/sitemap.xmlAuto-generated sitemap with all published entries
/api/docsInteractive Swagger UI