N
NodePress
/ Documentation
v1.4 Self-hosted REST API GraphQL Real-time MIT License

NodePress CMS

A self-hosted headless CMS. Define your content structure, fill it with data through the admin panel, then consume it via REST API, GraphQL, or real-time WebSocket 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

Have Docker? Skip this step and Step 5 — Docker will manage the database for you. Run docker-compose up -d inside your project folder after Step 4.
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-project with your project name. This downloads NodePress, generates secret keys, and installs all dependencies. Takes 2–5 minutes.

Using Docker? Add the --docker flag — npx create-nodepress-app my-project --docker — to include docker-compose.yml plus the nginx/monitoring configs and docker:* scripts. Without it, the project is set up for local PostgreSQL.

5

Connect NodePress to your database

Using Docker? Skip this step. Run docker-compose up -d in your project folder instead — Docker manages the database password automatically.

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-project/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 YOUR_NODEPRESS_DATABASE is the database name — you can name it anything you like.

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

Create the database tables

Run this from your project root. It creates all the tables NodePress needs. You only run this once.

Using a cloud database (Neon, Supabase, Railway)? Use migrate deploy instead: cd backend && npx prisma migrate deploy

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 dev server

Run this from your project root — it starts both backend and frontend together in one terminal:

Backend APIhttp://localhost:3000

Admin panelhttp://localhost:5173

Need to run them separately? Use npm run dev:backend and npm run dev:frontend in two terminal windows.

Root scripts (shortcut)

The root package.json has convenience scripts so you can run everything from the project root without cd-ing into subdirectories. The docker:* scripts are only generated when you scaffold with --docker:

ScriptWhat it does
npm run devStart both backend and frontend together in one terminal
npm run dev:backendStart backend dev server only (port 3000)
npm run dev:frontendStart frontend dev server only (port 5173)
npm run buildBuild both backend and frontend for production
npm run migrateRun Prisma migrations (prisma migrate dev)
npm run studioOpen Prisma Studio — visual database browser
npm run install:allInstall all dependencies (backend + frontend) — alias for npm install via npm workspaces
npm run docker:devStart Docker dev stack (docker-compose up)
npm run docker:prodStart Docker production stack with build
npm run docker:downStop all Docker containers
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":"Alice"}]
flexibleA list of blocks where each block can be a different layout.[{"_layout":"hero"}]
groupA single nested object with fixed sub-fields. Good for SEO metadata, address, social links.{"title":"My Post"}
relationLink to one or many entries in another content type. Stored as publicId UUID(s). Use ?populate= to inline the related data."uuid-v4" or ["uuid1","uuid2"]

Repeater — example schema & output

Define sub-fields once, add unlimited rows in the editor. Each item shares the same structure.

// Schema definition
{
  "name": "gallery",
  "type": "repeater",
  "subFields": [
    { "name": "image",   "type": "image", "required": true },
    { "name": "caption", "type": "text" }
  ]
}

// API output — an array of objects
"gallery": [
  { "image": "/uploads/photo1.jpg", "caption": "First photo" },
  { "image": "/uploads/photo2.jpg", "caption": "Second photo" }
]

Flexible — example schema & output

Each item in the list can be a different layout — perfect for page builders. The _layout key tells you which block type it is.

// Schema definition
{
  "name": "sections",
  "type": "flexible",
  "layouts": [
    {
      "name": "hero",
      "label": "Hero Banner",
      "fields": [{ "name": "heading", "type": "text" }]
    },
    {
      "name": "text_block",
      "label": "Text Block",
      "fields": [{ "name": "body", "type": "richtext" }]
    }
  ]
}

// API output — _layout tells you which block type it is
"sections": [
  { "_layout": "hero",       "heading": "Welcome to NodePress" },
  { "_layout": "text_block", "body": "<p>Some content here</p>" }
]

Group — example schema & output

A fixed set of sub-fields stored as a single nested object. Unlike repeater, there is no list — just one object. Perfect for SEO metadata, address blocks, or social links.

// Schema definition
{
  "name": "seo",
  "type": "group",
  "subFields": [
    { "name": "title",       "type": "text" },
    { "name": "description", "type": "textarea" },
    { "name": "og_image",    "type": "image" }
  ]
}

// API output — a single nested object, not an array
"seo": {
  "title": "My Post",
  "description": "A short summary of my post.",
  "og_image": "/uploads/og-cover.jpg"
}

Relation — example schema & output

Links entries across content types using their publicId UUID. Use ?populate=fieldName to inline the full related entry instead of just the UUID.

// Schema definition
{
  "name": "author",
  "type": "relation",
  "options": {
    "relatedContentType": "team",
    "cardinality": "one"
  }
}

// Default API output — returns the publicId UUID
"author": "a1b2c3d4-e5f6-4abc-8def-000000000001"

// With ?populate=author — returns the full entry inline
"author": {
  "slug": "jane-doe",
  "data": { "name": "Jane Doe", "role": "Editor" }
}

// cardinality: "many" — array of UUIDs or populated entries
"tags": ["uuid-1", "uuid-2"]

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

entry.restored

entry.purged

media.uploaded

media.deleted

* (all events)

Retry logic

Failed deliveries are retried up to 3 times: immediately, after 5 min, after 30 min. HMAC-SHA256 signature in X-NodePress-Signature header.

GraphQL API

NodePress exposes a full GraphQL API at /graphql alongside REST. Apollo Sandbox (interactive playground) is available in all environments — click GraphQL Playground in the Developer section of the admin sidebar.

Playground shows a blank page?

This is a browser cache issue — the browser cached a response with old security headers. Fix: open an Incognito window (works immediately) or press Ctrl+Shift+Delete → clear Cached images and files → refresh. Happens only once after a server restart.

Queries

Entry mutations

Content type mutations (admin only)

Webhook mutations (admin only)

Authentication

Public queries (entries, contentTypes) work without auth and return only published entries. Add Authorization: Bearer YOUR_JWT_TOKEN header for mutations and protected queries. Query depth is limited to 6 levels to prevent abuse.

Real-time (WebSocket)

NodePress broadcasts content changes over WebSocket using Socket.io at /api/realtime. Subscribe from your frontend to receive live updates without polling.

Events received

entry:created

entry:updated

entry:deleted

entry:restored

media:uploaded

media:deleted

Rooms

All connections join the global room automatically. Subscribe to a specific content type room to filter events:

ct:blog, ct:products
Authentication required. Every connection must send a JWT token or API key. Unauthenticated connections are immediately disconnected.

How to get your Bearer token

Option A — Login API: call POST /api/auth/login with your email + password. The response contains access_token. Use it as Bearer <access_token>.

Option B — Browser cookie: log into the admin panel, open DevTools → Application → Cookies → find np_token. That value is your Bearer token (valid for 7 days).

Option C — API key: create one in Admin → API Keys. Pass it as auth.apiKey — no expiry.

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 (7d) + sets refresh cookie (30d)

GET
/api/auth/me

Returns current user from token

Auth
POST
/api/auth/refresh

Exchange refresh token for new access token (silent rotation)

POST
/api/auth/forgot-password

Request password reset email. In dev without SMTP, returns devResetUrl in response.

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

GET
/graphql

GraphQL endpoint — Apollo Sandbox playground (GET) + API (POST). All environments.

GET
/api/metrics

Prometheus metrics (optional METRICS_TOKEN bearer auth)

Code Examples

Fetch blog posts (JavaScript)

React hook

Create an entry (with API key)

GraphQL query (JavaScript)

cURL