# Dynamic Collections

What the Visual Schema Builder creates, and how it coexists with collections defined in code.

A collection defined in `nextly.config.ts` exists because you wrote it. A collection built
in the [Visual Schema Builder](https://nextlyhq.com/docs/schema-builder.md) exists because someone created it in
the admin, and its definition lives in the database. The second kind is a **dynamic
collection**.

Both produce the same thing downstream: real database tables, the same REST and Direct API,
the same admin screens, the same access control. The difference is where the definition is
stored and who can change it.

## Availability

|            |                                                                                   |
| ---------- | --------------------------------------------------------------------------------- |
| Built in   | Created through the Visual Schema Builder at `/admin/builder/collections`         |
| Permission | Creating and editing schema is an admin capability, separate from editing content |

## Which to use

| Use code-first when                           | Use the Schema Builder when                                |
| --------------------------------------------- | ---------------------------------------------------------- |
| The shape is part of your application's logic | The shape is part of the site's content                    |
| You want it reviewed in a pull request        | You want a non-developer to add a field without a deploy   |
| It ships with your repository                 | It differs between environments, or is created by a client |

The two coexist. A project can define `posts` in code and let an editor add a `testimonials`
collection in the admin, and nothing about either changes because the other exists.

## Moving between them

A collection built in the admin can be moved into code with `nextly db:sync --promote <slug>`,
without recreating the table or losing content; only its slug and fields are carried over
([Collections](https://nextlyhq.com/docs/configuration/collections.md)). The other direction is not supported: a
code-first collection that has been synced shows read-only in the Schema Builder.

## What differs from a code-first collection

* Its **runtime definition** lives in a database row. The generated migration, however,
  carries both the table DDL and a serialized copy of the field definition, so once you commit
  it the shape *is* in git and *is* reviewable in a pull request.
* A metadata-only edit — renaming a label, changing help text — emits **no SQL migration**,
  because nothing about the table changed. In development the builder also mirrors the entity
  into a committable `ui-schema.json`, so such an edit usually still leaves something to
  review. That mirror is best-effort and development-only: treat the absence of a migration as
  the reliable signal, and the manifest as a convenience rather than a guarantee.
* The trade is narrower than "no oversight": structural changes arrive as a committed
  migration you can review, and presentational ones usually arrive in the development
  manifest.
* It **does** generate a SQL migration file under your `migrationsDir`, and that file is
  what recreates the table and its registry row in another environment. **Commit it.**
  Deploying without it leaves the collection unreproducible outside the database it was
  created in.
* A many-to-many relationship field stores its links in a junction table of its own, and the
  migration follows that table through the field's life: adding the field creates it, removing
  the field drops it (a field added later under the same name starts with no links), renaming
  the field — or changing its `junctionTable` — renames it, indexes and constraints included, so
  the links travel with the name; pointing the field at another collection replaces its table,
  and dropping the collection drops every junction the collection's own fields own. Two
  many-to-many fields cannot share one junction table, since a link does not record which field
  made it. Rename one many-to-many field per save — with two renamed in one save the builder
  cannot tell which old table belongs to which new field, and refuses rather than guess.

## Related

* [Visual Schema Builder](https://nextlyhq.com/docs/schema-builder.md) — the interface that creates these
* [Collections](https://nextlyhq.com/docs/configuration/collections.md) — the code-first equivalent
* [Production migrations](https://nextlyhq.com/docs/guides/production-migrations.md)
