Your first project
Create a Meridian project with init or init --from, migrate and apply it to a database, seed it, serve it and sign in to the admin as the first staff user.
A Meridian project is a directory: configuration under config/, templates under templates/, seed content under seed/ and static files under public/. This page creates one, puts it in a database and signs you in to the admin.
Each step needs the one before it:
flowchart LR init["init<br/>writes the files"] --> check["check --no-db<br/>no database"] check --> migrate["migrate up<br/>engine tables"] migrate --> apply["schema apply<br/>your types and sites"] apply --> seed["seed apply<br/>the home page"] seed --> dev["dev up<br/>serves the site"] migrate --> user["user create<br/>admin sign-in"]
You need the binary and a PostgreSQL database (Install).
Create the project
engine-dmo init my-site created a project in /home/you/my-site from the starter (...)
guidance for coding agents: AGENTS.md, .claude/skills/meridian-project/SKILL.md, .claude/skills/meridian/SKILL.md
editor support: config schemas in .meridian/schema, mapped for zed and vscode
next:
export DATABASE_URL=postgres://...
engine-dmo migrate up --project /home/you/my-site
engine-dmo schema apply --project /home/you/my-site
engine-dmo seed apply --project /home/you/my-site
engine-dmo serve --dev --project /home/you/my-site The starter is the smallest project that passes engine-dmo check and serves a page: one locale, one site, the core page type routed at /{slug}, a home page in the seed, and four templates (the layout base.html, page/entry.html, and the error pages errors/404.html and errors/500.html).
init takes the directory as its argument (the current directory when omitted) and refuses a directory that already holds config/engine.yaml.
| Flag | Default | Effect |
|---|---|---|
--from <dir> | none | Copy this project directory instead of writing the starter |
--editors <list> | zed,vscode | Map the configuration files to their JSON Schemas in these editors’ project settings (.zed/settings.json, .vscode/settings.json), so the editor completes and validates keys. Pass --editors "" for none |
--agents | true | Write guidance for coding agents: AGENTS.md and the meridian-project skill (both generated by engine-dmo guide from this project), and the meridian skill (the handbook’s For agents page). Each is written only when the file is not already there. --agents=false writes none |
What the starter contains
config/engine.yaml reads everything that differs between machines from the environment:
engine:
version: "1.x"
environment: ${ENVIRONMENT:-development}
server:
listen: ${LISTEN_ADDR:-127.0.0.1:8080}
base_url: ${BASE_URL:-http://localhost:8080}
database:
url: ${DATABASE_URL}
logging:
format: ${LOG_FORMAT:-text}
storage:
driver: filesystem
filesystem:
root: ./storage config/locales.yaml declares one locale, en. Set its timezone and currency to your own before you add content; the starter uses Etc/UTC and EUR:
locales:
available:
- code: en
id: loc_3990wq8rss
label: English
source: true
default: true
timezone: Etc/UTC
currency: EUR config/sites/main/site.yaml declares the one site, answering on localhost and localhost:8080, and routes.yaml routes pages and names the home page:
routes:
- type: page
id: rte_8397jasp17
uri: "/{slug}"
home: { type: page, slug: home } The id: values are written by init and are different in every project; never copy them between projects. Stable ids explains why.
config/access/grants.yaml gives the administrators group the built-in owner role, so the first staff user you create can do everything (Users, roles and permissions):
grants:
- { group: administrators, role: owner } seed/seed.yaml names the seed and lists locales: [en]. That list limits only the analytics a seed can generate; document values load in every locale a seed file gives, so a new locale needs its values in the documents, not in this list.
templates/_shared/page/entry.html renders a page:
{% extends "base.html" %}
{% block body %}
<h1>{{ entry.title }}</h1>
{% endblock %} The core page type has only two fields, title and slug. To give pages a body or a summary, extend the type in config/schema/ (Types); engine-dmo describe type page lists the fields a type has.
Replace the starter layout
templates/_shared/base.html is a placeholder that writes its own <title> and canonical link, so check warns that it has no head(). Replace its head with head(), which writes the title, description, canonical link, hreflang links, robots and social tags from the page and the site’s SEO settings:
<!doctype html>
<html lang="{{ locale.code }}" dir="{{ locale.direction }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ head() }}
</head>
<body>
<main id="main">{% block body %}{% endblock %}</main>
</body>
</html> SEO and structured data covers what head() prints.
Put it in a database
Point the engine at the database
cd my-site
export DATABASE_URL='postgres://meridian:meridian@localhost:5432/meridian?sslmode=disable' Every command reads the project in the current directory, or the one --project names.
Check the project
engine-dmo check --no-db check validates every configuration file, the schema and every template, and prints each problem with its file, line and column. It exits 0 when the project is valid and 3 when it is not. --no-db skips the checks that need a database and does not need secrets to be set.
The unchanged starter passes and prints one warning, that base.html has no head() (Replace the starter layout). The summary line still says 0 errors, 0 warnings: template warnings are printed above it but not counted, and --strict does not fail on them. Read every line check prints, not only the summary.
Run the migrations
engine-dmo migrate up Migrations create the engine’s own tables: core’s and those of every module the binary compiles. They only ever move forward.
Apply the schema
engine-dmo schema apply This reconciles the database with your configuration: locales, sites, types, fields, routes, roles and grants. engine-dmo schema plan shows the same changes without applying them.
Seed it
engine-dmo seed apply The starter’s seed creates and publishes the home page. Seeding goes through the same validation and revision history as an editor’s save, and refuses a database that already holds documents or media; engine-dmo seed reset --confirm empties content, media and analytics first (never in production).
Serve it
engine-dmo dev up http://localhost:8080 now serves the home page. The page home: { type: page, slug: home } names in routes.yaml answers /, not /home, which is not found. dev up serves the site in development mode and watches config/: each time you save a file it writes any missing ids, checks the project, plans the schema change and applies it when it is not destructive. Stop it with Ctrl+C. The development loop covers it and engine-dmo serve --dev.
Sign in to the admin
The admin is at /studio on any of the project’s sites. Staff users are created from the command line:
engine-dmo user create --email you@example.org --name "Your Name" first user: added to the administrators group
created you@example.org (...) in groups administrators
enrolment link, valid 24 hours, single use:
http://localhost:8080/studio/enrol/... The first user created goes into administrators, which the starter’s grants.yaml gives the owner role. Later users name their groups with --group (repeatable). The link is built from server.base_url.
Open the link, choose a password of at least 12 characters, scan the code into an authenticator app and enter a code from it. The admin then shows your recovery codes once. Staff accounts require a second factor by default.
user create needs the migrations applied. It does not send mail: it prints the link.
Development shortcuts
Two settings make signing in quicker on your own machine. Neither is allowed in production.
A file in seed/users/ creates a staff user when the seed is applied, with a password and an authenticator secret, so you can sign in straight away after every reseed:
# seed/users/admin.yaml. Development only: seed apply skips users in production.
email: admin@example.test
name: Dev Admin
groups: [administrators]
password: correct horse battery staple
totp_secret: JBSWY3DPEHPK3PXP totp_secret is a base32 secret you add to your authenticator app. The value above is a well-known example; use your own. Leave it out and the user cannot sign in while a second factor is required.
What to do next
- Add a type, a field or a route with the
engine-dmo devverbs whiledev upruns: The development loop. - Learn what each file is for: Project layout.
- Model your content: Content modelling.
- Write templates: Templates.
- Point your coding agent at
AGENTS.mdand the two skillsinitwrote; theengine-dmo devverbs regenerate them whenever they change the configuration.
Install
What Meridian needs to run (the engine binary or container image, PostgreSQL 14 or later, and optional search and mail services) and how to check an installation.
Project layout
Every directory and file a Meridian project can hold, what each is for, which bundle reads it, and which files you write and which the tooling writes.