{ type: "node" }

Everything is a Node

One recursive JSON structure for everything. Pages, tables, content, and files.

The Coreless Protocol

A strict, human-readable JSON schema. Every object follows the same Node interface.

Recursive Structure

Folders contain children. Children can be folders. Infinite nesting.

Strict Typing

Every node has a defined type and predictable data schema.

# Coreless Node Interface
interface Node {
  type: "folder" | "text" | "image" | "table";
  name: string;
  slug: string; // kebab-case
  data: Record<string, any>;
  children?: Node[];
}

Native Node Types

Built-in primitives to construct any content model without complex migrations.

folder

{ children: [...] }

text

{ data: { content: "..." } }

image

{ data: { url: "...", size: 1024 } }

table

{ data: { fields: [...] }, children: [row] }

row

{ data: { [slug]: value } }

article

{ data: { content: "# Markdown..." } }

Universal Architecture

Whether it's a Landing Page (folders & blocks) or a Knowledge Base (tables & rows), it's all just a tree of nodes.

Landing Page Structure

folder (landing)
├─ folder (hero)
│ ├─ text (title)
│ └─ image (cover)
└─ folder (about)

Knowledge Base Structure

folder (docs)
├─ table (categories)
└─ table (articles)
  └─ row (article_1)

JSON Import / Export

Since the entire structure is just a JSON tree, you can export your entire project state to a file, commit it to Git, or import it into a new environment instantly.

npm package

coreless-lib — official client

TypeScript / JavaScript library on npm. Wraps the same HTTP API; use it anywhere from static HTML to Next.js App Router. React is optional.

  • Framework-agnostic core — import from coreless-lib or coreless-lib/core
  • ContentClient: .text(), .image(), .video(), .file(), .list(), .table(), .field(), .node() by slash path
  • Tables: offset/limit, exclude_fields, search, filter — passed as query params on fetch
  • Coreless extras: getNode(path), getRow(page, table, row), getTable(page, table, params)
  • useContent(slug) for CSR; for RSC use await new Coreless(...).getPage(slug) in a Server Component
  • Peer dependency: react — optional; install only if you use CorelessProvider / useContent

Install

npm install coreless-lib

Coreless class

HttpContentFetcher calls GET /content/{projectSlug}/{path}. Coreless.getPage() returns a ContentClient with path-based helpers.

import { Coreless } from 'coreless-lib';

const cms = new Coreless({
  apiHost: process.env.NEXT_PUBLIC_API_URL!,
  projectSlug: 'my-project',
});

const page = await cms.getPage('home');
page.text('hero/title');
page.table('pricing/plans', { offset: 0, limit: 10 });

React

CorelessProvider + HttpContentFetcher at the app root; useContent(pageSlug) exposes the same methods plus loading, error, and refresh().

import { CorelessProvider, HttpContentFetcher, useContent } from 'coreless-lib';

const fetcher = new HttpContentFetcher({
  apiHost: 'https://api.example.com',
  projectSlug: 'my-project',
});

// <CorelessProvider fetcher={fetcher}>…</CorelessProvider>

function Home() {
  const c = useContent('home');
  if (c.loading) return <p>…</p>;
  return <h1>{c.text('hero/title')}</h1>;
}
API

How your app reads content

Same idea as Project → API docs: one JSON tree per page, then typed helpers for each field path.

Example only: project slug acme-studio, page home — paths match folder/block slugs in the tree.

REST

Public content endpoint returns the full page tree. Matches GET used by the dashboard (see api.nodes.getContent).

GET /content/acme-studio/home
Authorization: Bearer <access_token>
Accept: application/json

Typed helpers

After cms.getPage(pageSlug), use .text(), .image(), .table()… with paths like hero/title — the same shape auto-generated in project API docs.

import { cms } from '@/lib/cms';

const page = await cms.getPage('home');

const headline = page.text('hero/title');
// string | undefined

const lead = page.text('hero/lead');
// string | undefined

const cover = page.image('hero/cover');
// { url, width?, height?, filename?, size?, content_type? } | undefined

const pricing = page.table('sections/pricing');
// { fields: [...], rows: { _id, _slug, ...data }[] } | undefined

Fetch from your frontend

Direct fetch with the same URL and auth headers as in lib/api.ts.

import { getHeaders } from '@/lib/api';

const API_URL = process.env.NEXT_PUBLIC_API_URL;

const res = await fetch(
  `${API_URL}/content/acme-studio/home`,
  { headers: getHeaders() }
);
const tree = await res.json();