For Developers

A content layer that works the way your codebase does.

Schema in code. Type-safe client. Embedded deployment. No ops overhead. Dyrected fits into the way you already build — without forcing a separate service, a GUI schema builder, or a database you have to babysit.

schema

export default defineConfig({
  collections: [
    defineCollection({
      slug: 'posts',
      fields: [
        { name: 'title', type: 'text', required: true },
        { name: 'body', type: 'richText' },
        { name: 'author', type: 'relationship', collection: 'team' },
        { name: 'status', type: 'select',
          options: ['draft', 'published'] },
      ],
      access: {
        read: () => true,
        create: ({ user }) => user?.role === 'editor',
        update: ({ user }) => user?.role === 'editor',
        delete: ({ user }) => user?.role === 'admin',
      },
    }),
  ],
})

embedded

// Next.js — app/cms/[...dyrected]/route.ts
import { handler } from '@dyrected/next'
import config from '../../../dyrected.config'

export const { GET, POST, PUT, DELETE } = handler(config)

sdk

import { createClient } from '@dyrected/sdk'

const cms = createClient({
  baseUrl: process.env.CMS_URL,
  apiKey: process.env.API_KEY,
})

// Fully typed — fields, filters, relationships
const posts = await cms.collection('posts').find({
  where: { status: { equals: 'published' } },
  populate: ['author'],
  limit: 10,
})

access

access: {
  read: () => true,
  update: ({ user, doc }) =>
    user?.id === doc.author || user?.role === 'admin',
  delete: ({ user }) => user?.role === 'admin',
}

hooks

hooks: {
  afterChange: [
    async ({ doc, operation }) => {
      if (operation === 'create') {
        await notifySlack(
          `New post published: ${doc.title}`
        )
      }
    },
  ],
}

auth

defineCollection({
  slug: 'customers',
  auth: true,
  fields: [
    { name: 'name', type: 'text' },
    { name: 'plan', type: 'select',
      options: ['free', 'pro'] },
  ],
})

blocks

{ name: 'sections', type: 'blocks',
  blocks: ['hero', 'features', 'cta'] }

preferences

// Read active preference (personal → global → default)
const pref = await cms.getPreference('list-columns:posts')

// Save a personal preference
await cms.setPreference('list-columns:posts', ['title', 'status', 'publishedAt'])

// Publish a team-wide default (admin role required)
await cms.setPreference('list-columns:posts', ['title', 'status', 'publishedAt'], {
  scope: 'global',
})

Open-source core.

Full source on GitHub

Full source on GitHub

Self-host for free

Self-host for free

Managed cloud available

Managed cloud available

BSL license

BSL license

No vendor lock-in

No vendor lock-in

A CMS that fits in your repo, not beside it.

Schema in TypeScript. Deployed with your app. Types generated from your config.