Skip to content

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.

  • No secret. The address and the tokens of each tool are stored encrypted in the town hall. A key under meta that 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.
  • It lives in the data directory of the town hall (/data/myrtille.yaml in the image), or at the path of MYRTILLE_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.
  • 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 turns unknown: the town never shows something as fine when it does not know.
# myrtille.yaml (schema: https://myrtille.eu/schemas/town-plan/v1.json)
version: 1
town:
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] }
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.

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 ( :3000 for a port, (docker) for a container, (local)), in lower case, each run of other characters as one -: api :3000, api (docker) and api match. To pair a local service with a twin of another name, rename the local citizen in citizens[] by its exact id (local:<agent>:<repository>-<port> or local:<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 own local:<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 overlay greys 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).
  • couriers[].discover. With true (the default), every resource a courier reports joins the town. With false, a resource joins only when citizens lists 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) and otel. Claude Code sessions and local services come through the local agents, which connect on their own; couriers[].agents is read on a coolify courier, where it names the Docker agents of its servers.
  • mail.quietHours. Between from (included) and to (excluded), in the time zone of the town (town.timezone), the notification of an item whose urgency is not in except waits: 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. A critical item always rings, listed in except or 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. A to earlier than from spans midnight; a to equal to from sets no quiet hours.
  • mail.defaults.incidentTtlHours. With the defaults block, 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-1 to plant-3, poster-1 to poster-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.tileset and rendering.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.

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