N
NodePress / Documentation
v1.0 Self-hosted REST API

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.

1

Install Node.js

Already installed? Run 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.org

After installing, verify: node -v should print something like v22.0.0

2

Install Git

Already installed? Run 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.com

On Windows: click Next through all the options — the defaults are fine.

3

Install PostgreSQL

Already installed? Make sure you remember the password you set for the 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.
4

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.

5

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.

Didn't set a password? Try leaving it out:
DATABASE_URL="postgresql://postgres@localhost:5432/nodepress"
6

Create the database tables

This command sets up all the tables NodePress needs. You only run this once.

Getting an authentication error? The password in DATABASE_URL doesn't match your PostgreSQL password. Go back to Step 5 and check it.
7

Start the backend

The backend API is now running at http://localhost:3000. Keep this terminal open.

8

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.

9

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

1

Create a content type

Go to Content Types → New. Give it a name like blog and add fields: title (text), body (richtext), published (boolean).

2

Add an entry

Go to Entries → blog → New Entry. Fill in the fields. A URL-friendly slug is auto-generated from the title.

3

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 Postsblog_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}.

Reserved names: auth, media, entries, content-types, uploads — these are blocked to avoid route conflicts.

Field Types

TypeDescriptionJSON value
textShort single-line text. Good for titles, names, labels."My Blog Post"
textareaMulti-line plain text. Good for short descriptions."A short summary..."
richtextHTML from WYSIWYG editor. Supports headings, images, links."<p>Hello</p>"
numberInteger or decimal number.42
booleanTrue/false toggle. Good for published, featured flags.true
selectOne value from a predefined list of choices."tech"
imageA URL string pointing to an image (from Media Library or external)."/uploads/photo.jpg"
repeaterA list of items, each sharing the same sub-fields.[{"name":"Kartik"}]
flexibleA 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

JPEGPNGGIFWebPPDFMP4

Limits

Max file size: 10MB. Images are resized to a max of 2400px and converted to WebP automatically.

Storage: Files are saved locally to 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 levelCan doRate limit
readGET requests only120 req/min
writePOST / PUT / PATCH / DELETE60 req/min
allRead + Write combined120 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

VariableDescription
DATABASE_URLPostgreSQL connection string required
JWT_SECRET64+ char random secret for auth tokens required
CORS_ORIGINAllowed frontend origin (comma-separated for multiple) required
PORTAPI port (default 3000)
APP_URLBackend URL — used in API responses
SITE_URLPublic site URL — used in sitemap.xml
REDIS_URLRedis URL — enables shared cache (optional)
STORAGE_DRIVERlocal (default) or s3
STORAGE_S3_BUCKETS3/R2/MinIO bucket name (if STORAGE_DRIVER=s3)
SMTP_HOSTSMTP server for password reset emails
METRICS_TOKENBearer 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

POST
/api/auth/login

Email + password → returns JWT access token

GET
/api/auth/me

Returns current user from token

Auth
POST
/api/auth/refresh

Exchange refresh token for new access token

Content (Public)

GET
/api/:type

List all published entries for a content type. Supports ?page, ?limit, ?status

GET
/api/:type/:slug

Get a single published entry by slug

POST
/api/:type

Create a new entry

Auth
PATCH
/api/:type/:slug

Update an entry

Auth
DELETE
/api/:type/:slug

Soft-delete an entry

Auth

Media

GET
/api/media

List all uploaded files

Auth
POST
/api/media/upload

Upload a file (multipart/form-data, field: file)

Auth
DELETE
/api/media/:id

Delete a file by ID

Auth

Other

POST
/api/submit/:slug

Submit a form (no auth required)

GET
/api/health

Health check — DB connectivity

GET
/api/sitemap.xml

Auto-generated sitemap with all published entries

GET
/api/docs

Interactive Swagger UI

Code Examples

Fetch blog posts (JavaScript)

React hook

Create an entry (with API key)

cURL