# 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. ```ts 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 a `path` template 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 `slug` is `null`. 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](/platform/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](/concepts/revisions) record the `collectionVersionId` they were written under, so historical content always pairs with the schema that validated it. ```bash $ 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](/management-api/errors) and [CLI commands](/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](/concepts/previews) creation substitutes an entry's slug into the template to build the `{path}` placeholder of your customer preview URL.