Why We Built Nextly

N
Nextly Team
··3 min read

Every content tool we tried made us pick a side.

Code-first tools like Payload gave us what developers want: the schema in the repository, reviewable in a pull request, typed end to end, versioned with the code that depends on it. The cost landed on everyone else. A marketer who wanted one more field on a landing page filed a ticket and waited for a deploy.

Visual tools like Strapi and WordPress solved that and created the opposite problem. Anyone could add a field, which meant nobody could review one. The schema lived in a database we could not diff, drifted from the types in our editor, and turned every migration into an archaeology exercise.

Both camps are right about what they optimise for. We wanted to stop choosing.

What we wanted

One schema, reachable two ways.

A developer should be able to write a collection in TypeScript, review it in a pull request, and get types for it. An editor should be able to add a field in a browser without waiting for anyone. Both should be operating on the same thing, so neither has a second-class version of the product.

import { defineCollection, relationship, richText, select, text } from "nextly/config";

export const Articles = defineCollection({
  slug: "articles",
  admin: { useAsTitle: "title" },
  fields: [
    text({ name: "title", required: true }),
    richText({ name: "body" }),
    relationship({ name: "author", relationTo: "users" }),
    select({
      name: "reviewStage",
      options: [
        { label: "New", value: "new" },
        { label: "In review", value: "in_review" },
        { label: "Approved", value: "approved" },
      ],
    }),
  ],
  status: true,
  access: {
    read: true,
    create: ({ roles }) => roles.includes("editor"),
  },
});

That collection can equally be built in the Visual Schema Builder, by someone who has never opened the repository, and the result is the same collection: same API, same types, same admin.

Three decisions that follow from it

Content goes in your database. Not ours. Postgres, MySQL or SQLite, chosen with DB_DIALECT and DATABASE_URL. We do not run a service that holds your content, so there is nothing to migrate off later and nothing to price per record.

The CMS runs in your app, not beside it. Installing a package rather than deploying a second system removes the network hop, the second set of credentials, and the second thing to keep up at 3am. Your server components read content with a function call.

Access control is a function, not a checkbox. It receives the resolved roles and permissions for the request, so a rule is code you can read and test:

import { defineCollection, text } from "nextly/config";

export const Invoices = defineCollection({
  slug: "invoices",
  fields: [text({ name: "reference", required: true })],
  access: {
    read: ({ roles }) => roles.includes("finance"),
    create: ({ roles }) => roles.includes("admin"),
    update: ({ roles }) => roles.includes("admin"),
    delete: ({ roles }) => roles.includes("admin"),
  },
});

What we did not build

We left things out on purpose, and the roadmap says which. There is no GraphQL API yet. Preview ships as draft links and an editor pane, but not as unsaved edits streaming into the frame while you type. There is no hosted offering, no plugin marketplace, and no editorial workflow beyond draft and published.

We would rather name those than imply them. Nextly is in alpha at 0.0.2-alpha.62, it is MIT licensed, and the parts that exist are the parts we would use ourselves.

If the tradeoff we picked is the one you have been wanting, try it. If it is not, we would genuinely like to know why: that is what Discussions are for.

Start building with Nextly

Free, open source, and yours to own. No sign-up required.

Terminal