Introduction
Strux is a Git-native, JSON-based Content Management System that turns your file system into a database and Git into your database engine. Instead of PostgreSQL or MongoDB, all content lives as plain JSON files versioned by Git.
Content-as-Code: Everything — schemas, content, config — is a file. Version it, branch it, merge it, diff it. Just like your code.
Why Strux?
- Zero database infrastructure — no hosting, no backups, no migrations
- Native Git versioning — full history, author attribution, branch-based staging
- Perfect CI/CD integration — frontends read from the file system at build time
- Developer-first — JSON schemas, REST API, TypeScript throughout
Installation
Quick Start (Recommended)
npx create-strux-app my-project
cd my-project
pnpm install
pnpm dev
Manual Setup
Clone the repository and install dependencies manually:
git clone https://github.com/wahidzzz/strux-cms.git my-project
cd my-project
pnpm install
pnpm build
pnpm dev
Requirements
| Dependency | Version |
|---|---|
| Node.js | ≥ 20.0.0 |
| Git | ≥ 2.30 |
| pnpm (recommended) | ≥ 8.0 |
Project Structure
Strux is organized as a Turborepo monorepo with three packages:
my-project/
├── packages/
│ ├── core/ # Core engines (FileEngine, GitEngine, etc.)
│ ├── api/ # REST API layer
│ └── admin/ # Next.js admin interface
├── content/ # Content storage (JSON files)
│ └── api/ # Organized by content type
├── schema/ # Content type definitions
│ ├── article.schema.json
│ ├── category.schema.json
│ └── profile.schema.json
├── uploads/ # Media file storage
├── .cms/ # System files (RBAC, users, index)
├── package.json # Root workspace config
└── turbo.json # Build pipeline
Package Overview
| Package | Purpose | Key Tech |
|---|---|---|
@cms/core | Business logic, data engines | TypeScript, AJV, Chokidar |
@cms/api | REST API endpoints | Express, JWT, bcrypt |
@cms/admin | Admin panel UI | Next.js 14, Tailwind, Radix UI |
Collection Types
Collection types represent content that can have multiple entries — like blog posts, products, or users. Each collection type is defined by a JSON Schema file in the schema/ directory.
Defining a Collection Type
{
"displayName": "Article",
"kind": "collectionType",
"singularName": "article",
"pluralName": "articles",
"description": "Blog posts and news articles.",
"apiId": "article",
"attributes": {
"title": {
"type": "string",
"required": true
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true
},
"content": {
"type": "richtext",
"required": true
},
"is_featured": {
"type": "boolean",
"required": false
}
}
}
Supported Field Types
| Type | Description | Example |
|---|---|---|
string | Short text | Titles, names |
text | Long text | Summaries, excerpts |
richtext | Rich text (HTML) | Article body |
number | Integer or float | Price, count |
boolean | True/false | Published, featured |
date | Date string | Published date |
json | Arbitrary JSON | Metadata, tags |
uid | Unique identifier/slug | URL slug |
media | File reference | Images, documents |
relation | Content relationship | Author → Articles |
component | Reusable component | SEO meta, address |
dynamiczone | Flexible block area | Page sections |
Single Types
Single types represent content with only one entry — like global settings, a homepage, or site metadata.
{
"displayName": "Global Settings",
"kind": "singleType",
"singularName": "global-settings",
"attributes": {
"site_name": { "type": "string", "required": true },
"tagline": { "type": "string" },
"logo": { "type": "media" },
"social_links": { "type": "json" }
}
}
Note: Single types always have exactly one entry. The admin panel shows an edit form instead of a list view.
Relationships
Strux supports four types of relationships between content types:
one-to-one
A single entry relates to exactly one entry in another type.
// In user.schema.json
"profile": {
"type": "relation",
"relation": {
"target": "profile",
"relation": "oneToOne"
}
}
one-to-many
One entry can relate to many entries in another type.
// In author.schema.json
"articles": {
"type": "relation",
"relation": {
"target": "article",
"relation": "oneToMany"
}
}
many-to-one
Many entries relate to one entry (the inverse of one-to-many).
// In article.schema.json
"category": {
"type": "relation",
"relation": {
"target": "category",
"relation": "manyToOne"
}
}
many-to-many
Multiple entries can relate to multiple entries in another type.
// In article.schema.json
"tags": {
"type": "relation",
"relation": {
"target": "tag",
"relation": "manyToMany"
}
}
Population
Relationships are resolved at query time using the populate query parameter:
GET /api/articles?populate=author,category
GET /api/articles?populate=* // Populate all relations
Dynamic Zones
Dynamic zones let editors compose pages from reusable content blocks (components). Each block has its own schema and can be added, removed, or reordered by editors.
Defining Components
Components live in the schema/components/ directory:
// schema/components/hero.json
{
"displayName": "Hero",
"attributes": {
"heading": { "type": "string", "required": true },
"subheading": { "type": "text" },
"background_image": { "type": "media" },
"cta_text": { "type": "string" },
"cta_url": { "type": "string" }
}
}
Using Dynamic Zones in Schemas
// In page.schema.json
"body": {
"type": "dynamiczone",
"components": ["hero", "feature-grid", "testimonials", "cta"]
}
Content Structure
Dynamic zone content is stored as an array of typed blocks:
{
"body": [
{
"__component": "hero",
"heading": "Welcome to Our Site",
"subheading": "We build amazing things"
},
{
"__component": "feature-grid",
"features": [...]
}
]
}
Content Modeling Guide
Effective content modeling is the foundation of a good CMS experience. Here are best practices for Strux:
1. Start with the API Consumer
Think about how your frontend will consume the data. Design schemas that match your component structure.
2. Use Components for Reusable Structures
If multiple content types share the same fields (e.g., SEO metadata, address blocks), extract them into components.
3. Choose the Right Relationship Type
| Scenario | Relationship |
|---|---|
| User has one profile | one-to-one |
| Author writes many articles | one-to-many |
| Article belongs to one category | many-to-one |
| Article has many tags, tag has many articles | many-to-many |
4. Use Dynamic Zones for Flexible Pages
For landing pages or marketing pages where the layout varies, use dynamic zones instead of fixed fields.
API Usage
Strux provides a REST API for full CRUD operations on your content.
Base URL
http://localhost:3000/api
Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/:pluralName | List all entries |
GET | /api/:pluralName/:id | Get single entry |
POST | /api/:pluralName | Create entry |
PUT | /api/:pluralName/:id | Update entry |
DELETE | /api/:pluralName/:id | Delete entry |
Query Parameters
// Pagination
GET /api/articles?page=1&pageSize=10
// Sorting
GET /api/articles?sort=createdAt:desc
// Filtering
GET /api/articles?filters[is_featured]=true
// Population
GET /api/articles?populate=author,category
// Search
GET /api/articles?search=hello
Authentication
// Register
POST /api/auth/register
{ "email": "user@example.com", "password": "secure123", "username": "user1" }
// Login
POST /api/auth/login
{ "email": "user@example.com", "password": "secure123" }
// Returns: { "token": "jwt-token-here", "user": { ... } }
// Use token in subsequent requests
Authorization: Bearer jwt-token-here
Deployment Guide
Strux can be deployed anywhere Node.js runs. Here are the most common deployment patterns:
Self-Hosted (VPS / VM)
# Build for production
pnpm build
# Start the server
NODE_ENV=production pnpm start
# Or use PM2
pm2 start npm --name "strux" -- start
Docker
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install -g pnpm && pnpm install && pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]
Environment Variables
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | Server port |
JWT_SECRET | — | Secret for JWT signing (required) |
NODE_ENV | development | Environment mode |
CONTENT_DIR | ./content | Content storage path |
SCHEMA_DIR | ./schema | Schema definitions path |
Important: Always set a strong JWT_SECRET in production. Never commit it to your repository.