Town plan
The town plan, myrtille.yaml, says how your tools map onto the town: which courier feeds which district and building, which citizens to rename or group, and the citizens you declare by hand, such as a home-made automation. You do not write it from nothing: the setup of the town (or myrtille init) writes it from what Coolify discovers, with comments, and you edit it afterwards if you want to.
What the town plan never holds
Section titled “What the town plan never holds”- No secret. The address and the tokens of each tool are stored encrypted in the town hall. A key under
metathat looks like a secret is refused. You can commit the file to an infrastructure repository without risk. - No switch for actions. Turning on the actions of a district is never done in the file: an admin does it in the town, and it is recorded in the audit log.
Where it lives and how it is read
Section titled “Where it lives and how it is read”- It lives in the data directory of the town hall (
/data/myrtille.yamlin the image), or at the path ofMYRTILLE_TOWN_PLAN. - The town hall reads it again every 2 seconds and applies a change without a restart.
- It is strict: an unknown key is an error, except under
meta. A plan that does not validate keeps the previous one in force, and the mail gets a high item with the first problem, closed by the next valid plan. A first plan that does not validate stops the start. myrtille init, run again, only adds the districts and buildings that are missing, and keeps every line, comment and edit already there. The panel of a citizen in the town writes its name, avatar and building into the file, keeping your comments too.
Discovery creates, the file overrides
Section titled “Discovery creates, the file overrides”- A citizen a courier reports gets the name, kind, building and district the plan gives it, by its exact id:
<courier>:<kind>:<ref>. Discovered citizens need not be listed. - A discovered citizen its source stops reporting turns
offline, then leaves the town 24 hours later, even when the plan overrides it. - A citizen only the plan declares is never removed by discovery.
- With
expectedEvery, a citizen that gives no news for longer turnsunknown: the town never shows something as fine when it does not know.
A complete example
Section titled “A complete example”# myrtille.yaml (schema: https://myrtille.eu/schemas/town-plan/v1.json)version: 1town: id: travelyzer name: Travelyzer locale: en timezone: Europe/Paris
couriers: - id: coolify type: coolify instance: main # key to the encrypted config (URL + tokens) in the database discover: true # automatically adds new resources poll: idleSeconds: 45 activeSeconds: 3 - id: claude type: claude-code agents: [theo-laptop, nathan-desktop] # enrolled local agents - id: ops-hooks type: webhook # HMAC secret in the database, never here
districts: - id: production name: Production kind: environment source: { courier: coolify, environment: production } confirmation: always # actions are enabled in the interface (admin, logged), never here - id: staging name: Staging kind: environment source: { courier: coolify, environment: staging } confirmation: destructive - id: previews name: Previews kind: previews source: { courier: coolify, previews: true } - id: local name: Local kind: local overlay: { shadows: staging } # a local service greys out its staging twin
buildings: - id: compass name: Compass source: { courier: coolify, project: 'Compass B2B' } - id: app name: Travelyzer App source: { courier: coolify, project: 'Travelyzer' } - id: studio name: Content studio # building without a source: groups automations declared by hand
citizens: # Discovered citizens do not need to be listed. # Only overrides and citizens declared by hand are listed here. - id: 'coolify:app:9c1e4b2a' name: API Gateway # renames avatar: engineer-02 building: compass - id: 'ops-hooks:tiktok-generator' kind: automation name: TikTok generator building: studio district: production avatar: creator-01 expectedEvery: 6h # switches to unknown if it gives no news actions: - type: task.run label: Start a generation webhook: https://ops.example.com/run/tiktok # outgoing action (signed) - id: 'claude:agent:compass' kind: agent name: Agent Compass building: compass district: local repo: { path: ~/code/compass, agent: theo-laptop, localPorts: [3000, 5173] } # localPorts: local overlay detection missions: { enabled: true, maxConcurrent: 1, branchPrefix: mission/ }
mail: quietHours: { from: '22:00', to: '07:00', except: [critical] } defaults: { incidentTtlHours: 24 }
rendering: layout: auto # auto | manual tileset: default palette: endesga32 decor: - { building: studio, props: [plant, poster-travel] }The sections
Section titled “The sections”| Section | Required | What it holds |
|---|---|---|
version |
yes | Always 1. |
town |
yes | A stable id; the language and time zone of the reports. |
couriers |
yes | At least one source. The credentials live in the town hall. |
districts |
yes | One per environment; confirmation is always, destructive or never, and never is refused in production. |
buildings |
no | Generated from the projects; a building without a source only groups citizens. |
citizens |
no | Overrides by id, and citizens declared by hand. |
mail |
no | Quiet hours and lifetimes. |
rendering |
no | layout: manual keeps the positions (x, y in tiles) given for districts, buildings and citizens. |
A district whose id or environment is named prod, production or live counts as production. Without confirmation, a production district asks for a confirmation before every action, and another district before destructive actions only.
The town plan reference lists every field, its type, whether it is required and its default. It is generated from the JSON Schema of the file.
The local overlay is visual only
Section titled “The local overlay is visual only”A district of kind local holds what runs on the workstations of the team: the ports a local agent watches for the repositories of the plan (repo.localPorts) and the containers of its local Docker. With overlay: { shadows: staging }, a local service that is running greys out its twin in staging: the citizen of that district with the same logical name is drawn as a see-through ghost with the badge duplicated locally by <developer>, in the town, in the list of citizens and in its panel.
- Nothing is redirected. No traffic of staging reaches the workstation, no tunnel is opened, no proxy rule is written. The ghost only tells the team that someone is working on a copy of that service. Real routing is a separate feature for a later major version (ADR-0022).
- The twin keeps its real state. A ghost still shows its state with its icon; a ghost that fails, waits for a decision or gives no news says so in its bubble. Otherwise its bubble names who duplicates it, in place of the progress or the message of its work: open its panel to read them.
- The logical name is the name of the citizen without the marks of a local service (
:3000for a port,(docker)for a container,(local)), in lower case, each run of other characters as one-:api :3000,api (docker)andapimatch. To pair a local service with a twin of another name, rename the local citizen incitizens[]by its exact id (local:<agent>:<repository>-<port>orlocal:<agent>:docker.<container>) to the name of its twin. - The developer named on the badge is the name of the local agent of that workstation (
myrtille connect <address> --name <name>, by default the host name). Only the services an agent reports in its ownlocal:<agent>:namespace cast a ghost; the town hall refuses a local service an agent reports anywhere else, and a developer name an agent writes itself. - Production is never greyed out, even if a local district names it: the town never shows production other than it is. A local district without
overlaygreys out nothing. - The ghost disappears as soon as the local service stops, or when its agent is no longer heard (the town greys its local citizens after 90 s without news).
Discovery, mail and drawing
Section titled “Discovery, mail and drawing”couriers[].discover. Withtrue(the default), every resource a courier reports joins the town. Withfalse, a resource joins only whencitizenslists it by its exact id; a citizen already in the town stays and keeps its real state, so turning discovery off never greys out what runs. It applies to the couriers the town hall runs:coolify,dokploy,webhook,git-github,mcp(over HTTP) andotel. Claude Code sessions and local services come through the local agents, which connect on their own;couriers[].agentsis read on acoolifycourier, where it names the Docker agents of its servers.mail.quietHours. Betweenfrom(included) andto(excluded), in the time zone of the town (town.timezone), the notification of an item whose urgency is not inexceptwaits: it is pushed when the quiet hours end, if the item is still open, and not at all if someone decided, dismissed or let it expire meanwhile. Acriticalitem always rings, listed inexceptor not. The daily summary waits for the end too, within the three hours it may be late. The mail itself never waits: every item shows at once in the town and in/inbox, and the Notifications section says the quiet hours and whether they hold now. Atoearlier thanfromspans midnight; atoequal tofromsets no quiet hours.mail.defaults.incidentTtlHours. With thedefaultsblock, every incident with a fixed lifetime (a failed deployment, a container that crashed, a failed backup or task) expires after this many hours (24 by default). Without the block, a failed deployment and a container incident expire after 24 hours, a failed backup or task after 48. An incident that ends on an event (a server unreachable until it answers, a courier disconnected until it reconnects) waits for that event, and a permission keeps the expiry its agent asked for.rendering.decor. The props of a building are drawn on its back wall, left to right, ten at most, in every district where the building stands. A prop is a tile of the furniture (plant-1toplant-3,poster-1toposter-4,coffee-machine,shelf,lamp) or a family followed by any name (plant,poster-travel), which always gives the same tile for the same name. A prop the town has no tile for is left out, and the town hall logs a warning when it reads the plan. The decor appears at the next drawing of the map (a citizen who comes or leaves, or a reload of the page).rendering.tilesetandrendering.palette. The town ships one tileset,default, and one palette,endesga32(the palette of the art is fixed). Another name is drawn with these, and the town hall logs a warning when it reads the plan.
Autocompletion in your editor
Section titled “Autocompletion in your editor”The JSON Schema of the file is published at https://myrtille.eu/schemas/town-plan/v1.json. With the YAML extension of your editor, a first line such as this one turns on completion and checks as you type:
# yaml-language-server: $schema=https://myrtille.eu/schemas/town-plan/v1.json