Concepts
Collections and schema versions
Code-owned content models, public and private visibility, and immutable schema versions.
A collection defines the shape of a set of entries: its fields, validation rules, slug behavior, and how it is exposed. Collections are defined in code with @myna-sh/sdk/schema and deployed with myna schema push — the dashboard displays deployed schemas but never edits them. This keeps one source of schema truth, reviewable in the same pull requests as the code that consumes it.
import { collection, field } from "@myna-sh/sdk/schema";
export const posts = collection({
name: "posts", // immutable key
label: "Blog posts", // editable display name
kind: "collection", // or "singleton"
visibility: "public", // default: "private"
titleField: "title",
path: "/blog/{slug}",
fields: {
title: field.text({ required: true, maxLength: 120 }),
slug: field.slug({ from: "title", required: true }),
body: field.markdown({ required: true }),
},
});
Collections and singletons
kind is collection or singleton:
- A collection holds many entries. It gets slugs when it says its content lives at a URL — by declaring a
field.slug(), or apathtemplate that has to build one. Slugs are unique within the collection, validated (lowercase-kebab-case), auto-generated from the configured source field when omitted, and old slugs are preserved as aliases when an entry's slug changes. - A collection that declares neither has no slugs: its entries are addressed by id and
slugisnull. Nothing is required of you to get that — a collection of testimonials, feature flags, or pricing tiers is not a set of pages, and Myna does not ask it to invent URLs. Naming a slug when creating an entry in such a collection is refused rather than ignored, because the caller believes they have addressed something. - A singleton holds one document with no slug — site settings, a homepage, a footer. The public content API reads it at
GET /v1/projects/:project/collections/:collection/singleton.
Public and private visibility
visibility is public or private, defaulting to private:
| Visibility | Public content API | Management API |
|---|---|---|
public |
Published entries readable without credentials (when the project's public API is enabled) | Full access with scopes |
private |
Reads require a credential with content:read (AUTHENTICATION_REQUIRED otherwise) |
Full access with scopes |
GET /v1/projects/:project/collections is dual-mode: callers with schema:read get the full management view; everyone else gets only the public collection list. Switching a collection from public to private is classified conditionally_destructive in the schema diff because it can break consumers; public-ward changes are safe.
Immutable keys
A collection's key (the name in code) is immutable, as is every field key. Renaming in the schema source is not a rename to Myna — it is a remove plus an add, a destructive pair that drops the old data. Labels, descriptions, defaults, and other UI metadata are editable freely; keys are contracts.
Schema versions
Every push that changes a collection's schema creates an immutable collection_version with an increasing version number, the canonical schemaJson, and a schema_hash (SHA-256 of the canonical JSON). Versions are never edited in place, and (collection, schema_hash) is unique: pushing a schema identical to the deployed one is a no-op, and pushing a schema that matches an earlier version's hash rolls back by pointing the collection's current version at that existing version rather than minting a duplicate.
Entry revisions record the collectionVersionId they were written under, so historical content always pairs with the schema that validated it.
$ myna schema diff
# POST /v1/projects/:project/schema/diff — a plan, no writes
$ myna schema push --allow-destructive
# POST /v1/projects/:project/schema/push
diff classifies every operation as safe, conditionally_destructive, or destructive and validates all current draft and published entry heads against the proposed schemas. push refuses destructive changes without allowDestructive (SCHEMA_CHANGE_DESTRUCTIVE, HTTP 409) and refuses schemas that existing entries violate (VALIDATION_FAILED, HTTP 422) — fix or migrate the content first. See error codes and CLI commands.
Version history is inspectable at GET /v1/projects/:project/collections/:collection/versions, each version carrying actor attribution and an optional changeSummary.
Path templates
A collection may declare a path template such as /blog/{slug}. Myna does not route your site — the template exists so tooling can point at real pages: preview creation substitutes an entry's slug into the template to build the {path} placeholder of your customer preview URL.
