Repository navigation
Admin Technical Guide
For whoever keeps a DesKilo space running: the documents it prints, the files it exchanges, the services it talks to, and the database under it. The everyday configuration is in the configuration guide; what a member sees is in the user guide.
A slot marked <!-- image: … --> is a screenshot the pipeline has not
been given yet.
Every printed thing in DesKilo — an invoice, a reminder, a member letter, a consumption report, a VAT return, a badge sheet — comes out of one engine. A report kind names the document; a design says how it looks; the data the app hands it is a fixed vocabulary of placeholders.

The report editor: the language and document pickers on top, the Markup / Visual and Design / Preview switches under them, and the invoice's bands — header, body, footer — below.
Badge and space-QR covers use the same page engine as letters. They load images from the workspace library, repeat the continuation band when a cover spans several pages, and keep its footer at the bottom. The printable card grids keep their existing dimensions and QR payloads.
Business analytics PDF exports use the report designer for headers, introductory text, footers and shared images. The dashboard figures, charts and estimate explanations are retained in both banded and positioned designs.
Every report offers a Professional template alongside the existing choices (#1994). It keeps the complete report-specific sections; payment summaries keep confirmed and pending amounts separate, and consumption reports keep their usage records. Management reports do not request late-payment penalties. Existing custom designs stay in place until you explicitly replace them.
The automatic letter layout follows the workspace country (France: right window; Germany: left window) unless you selected an address-window override. Document language is separate from country. Templates display the supplied legal facts; appearance and translation alone do not establish legal compliance or satisfy electronic-invoicing obligations.
The quick way to design. Four bands of markup, each with its own job:
| Band | Where it prints |
|---|---|
| header | the top of page 1 only — the letterhead |
| continuation | the top of pages 2 and after — a strip naming the document |
| body | the only flowing zone: it is what runs on and paginates |
| footer | the bottom of every page |
Inside a band, one sign per line decides what the line is:
| Sign | What the line becomes |
|---|---|
# |
a heading |
## |
a subheading |
- |
a small line |
| a | b | |
a table row; a row of --- makes the one above a header |
--- |
a horizontal rule |
![name|w|align] |
an image from the library, with size and alignment |
| (blank) | a spacer |
| anything else | body text |
The designer's Placeholders and markup panel carries all of this inline, plus Insert a field… — the searchable picker grouped by topic, with a one-line meaning under every name, searchable by that meaning too — and three ready-made pieces: a line that prints only when its value exists, one row per invoice line, and the title that says invoice, credit note or proforma. Whatever you tap lands at the caret of the band you last edited.
Headers and body flow from the top down. The footer keeps a fixed bottom position on every page; additional footer lines grow upward. Only page one prints the full letterhead and addresses. Later pages use the continuation band and page numbers. The VAT declaration preparation PDF also uses these editable templates, retaining the saved amounts and filing status.
Use Insert an image to upload a logo, signature or other picture once to the workspace library, then select it in any report template. Images can be placed in any band with ![name] or in a positioned layout with <image name="name"/>. Uploading the same file name replaces that library image for future renders of every template that references it; previously saved PDFs remain unchanged.
Monthly bills use the shared page engine. Quick view retains services, packages, pending items and confirmed credits separately. Development-workspace bills carry the development watermark.
The workspace configuration PDF is also an editable report type. Its default template retains the overview, members and their roles, enabled features, availability, invitation settings, and level/office/desk/seat hierarchy. It can reuse workspace-library images and prints the development watermark when applicable.
The exact way. An XML layout places every element at a millimetre, for a document that has to satisfy a window envelope or a national form. A layout wins over the bands for the kind it is set on.
The root and its zones:
<report-layout version="1" page="A4" margin="20mm"
margin-top="8mm" margin-bottom="8mm">
<header height="…">…</header>
<continuation height="…">…</continuation>
<recipient window="fr|din|off"/>
<body y="90mm">…</body>
<footer height="…">…</footer>
</report-layout>margin is the side margin; margin-top and margin-bottom split the
vertical one when a document needs them apart, and are the side margin
when absent. <recipient> takes a named window — fr at 110 mm,
din at 20 mm, both 45 mm down in an 85 × 40 mm box — or explicit
x y w h, or off. <body y="…"> is the only flowing zone: y is
where it resumes, 90 mm under a window.
Elements, valid inside a zone, a <box> or a <column>:
| Element | What it does |
|---|---|
<text style="heading|subheading|body|small" align="left|center|right" bold="true"> |
a run of type |
<image name="library-name" fit="contain|cover|fill" align="…"/> |
a picture from the image library |
<table><col w="55%" align="right"/>…<row bold="true"><cell align="…">…</cell></row></table> |
a table with declared columns |
<box>…</box> |
a group, so children position inside it |
<columns><column>…</column>…</columns> |
side-by-side groups |
<rule/> |
a horizontal line |
<spacer size="4mm"/> |
vertical space |
<markup>…</markup> |
band markup, verbatim, inside a positioned layout |
Frame attributes — x y w h — apply to any element. With x or
y the element is placed absolutely inside its parent; without either,
it flows after its siblings.
Units are mm cm px pt %. A bare number is millimetres; px is the
CSS pixel (1/96 in); % is of the parent — width for x and w,
height for y and h.
Every placeholder the engine knows, per document family, with the loops
(lines, vat, usage_records, …) and the fields each row carries.
dart run tool/report.dart describe prints the current list — it is
generated from the same registry the renderer reads, so it can never be
out of date.
Liquid runs over the whole file first, before the XML is parsed, so a condition may open in one element and close in another. Values are XML-escaped on the way in.
| Form | What it does |
|---|---|
{{ field }} |
prints the value, escaped |
{% if field != "" %}…{% endif %} |
prints the block only when the field has a value |
{% if a == b %}…{% else %}…{% endif %} |
the two-branch form |
{% unless field == "" %}…{% endunless %} |
the negated form |
{% for line in lines %}…{% endfor %} |
one pass per row of a loop |
{{ forloop.index }} |
the 1-based row number inside a loop |
The rule that catches everyone: every placeholder the engine knows
is seeded empty, never nil. An absent field is "", so
{% if x != "" %} behaves and a design never prints the word nil.
Owner texts (text.<key>) are seeded the same way through their own
defaulting map.
Loops and their rows. lines gives label, kind, pct, month, qty, unit_price, net, vat_rate, amount, negative. month is the
subscription position's month, already translated into the document's
language — the reason an invoice line can read September 100 %.
vat gives the per-rate breakdown, usage_records the half-days,
vat_positions and vat_rate_totals the declaration's own rows.
A letter that goes in a window envelope has one geometry, and it is not a matter of taste:
| Thing | Where |
|---|---|
| sender line | 20 mm from the left, 20 mm down |
| recipient block | 110 mm from the left, 45 mm down, inside 85 × 40 mm |
| body | resumes at 90 mm |
| footer | on every page |
| continuation strip | from page two |
Nothing but the recipient may put ink inside the window band. This is proven on the rendered PDF, not by eye: the check measures the ink positions of the produced file and exits non-zero when something lands where the window is.
dart run tool/report.dart check <layout.xml> [--data data.json]
dart run tool/report.dart render <layout.xml> [--data data.json] -o out.pdf
dart run tool/report.dart sample --kind invoice > data.json
dart run tool/report.dart describe
- check renders the design and measures it against the window contract. Exit 0 conforms; exit 1 means ink in the window band, and it says which element; exit 2 means the design could not be read, and it names the element that broke.
- render produces the PDF, so a design can be proofed without the app.
- sample writes a data file carrying every placeholder the engine knows, which is the fastest way to see what a field is called.
- describe prints the vocabulary above — zones, elements, frame attributes, units, Liquid and the placeholder list. It is generated from the same registry the renderer reads, so it cannot drift from the engine.
The CLI is pure Dart and must stay that way: it imports nothing from
Flutter or the localizations, and a test breaks the moment a domain
file it uses pulls AppLocalizations in.
An invoice leaves DesKilo as a PDF a person reads and a structured file a machine reads, and the two say the same thing because they are produced from the same frozen document.
All three are the same invoice expressed three ways, and all three satisfy EN 16931, the European semantic model that says which facts an invoice must carry (BT-1 the number, BT-48 the buyer's VAT identifier, and so on).
| Format | What it is |
|---|---|
| CII | UN/CEFACT Cross Industry Invoice — the XML syntax Chorus Pro takes |
| UBL | OASIS Universal Business Language — the syntax Peppol takes |
| Factur-X | a PDF/A-3 with the CII XML embedded in it — one file a person reads and a machine parses |
Factur-X is why the PDF and the XML cannot disagree: they are the same file. When a platform wants them apart, both are produced from the one frozen document, never re-rendered from live data.
Before anything is transmitted the app checks the document against the norm and refuses with the missing item named, because an invoice rejected by a platform costs more to fix than one never sent.
What it refuses:
- a seller with no identifier the regime demands — a VAT number when you charge VAT, a company registration number when you do not;
- a reverse-charge document whose customer has no VAT number: that number is what proves the tax is theirs;
- an exemption with no reason and no country default to fall back on;
- a customer VAT number whose shape does not match its country — a warning, not a refusal, since shapes change;
- a buyer with no address, once the destination requires one.
Members supply their own country and, when they invoice as a business, their VAT number, beside their address in Settings → Personal information.
A document can go to two destinations at once: the government platform your country mandates, and the customer's own service. Both are configured on the space, and either may be off.
Credentials live on the workspace, never in the space file and never in a deployment — an export you send to a colleague carries the configuration and not the keys. A development space always uses the test endpoint, which physically cannot reach a government platform, so a test invoice can never become a real one.
Every attempt is recorded on the invoice's own transmission history: when, to which destination, what the platform answered, and the reference it returned. A failed transmission leaves the invoice untouched and retryable — the document is frozen, the transmission is not part of it.
Three formats, one ledger underneath:
| Format | Where it is asked for | What it carries |
|---|---|---|
| FEC | France (art. A47 A-1 LPF) | every entry of the period, in the mandated column order |
| SAF-T | the OECD standard, several EU countries | the audit file: accounts, entries, documents |
| DATEV | Germany, for the tax adviser's software | the entries in the layout DATEV imports |
All three cover a period you choose and use the chart of accounts configured on the space — the VAT account included, which is why that field belongs to the legal identity rather than to the export. A period already exported is not locked: an export is a read, and it can be taken again after a correction.
| Integration | What it does | Without it |
|---|---|---|
| Payment provider | takes a payment against an invoice | payments are recorded by hand; nothing else changes |
| opens WhatsApp on the device: the space's group link and the number a member chooses to share; the server sends nothing to WhatsApp (0136) | no WhatsApp button is shown | |
| Push | delivers notifications to a device | notifications appear when the app is opened |
| E-invoicing platform | transmits the structured invoice | the PDF is produced and sent by other means |
Two rules hold for all of them. Credentials live on the workspace, in a table the space file and every deployment skip, so no export ever carries a key. And an unconfigured integration degrades, it does not break: the feature that needs it is switched off, the screen says so, and nothing throws.
There is no e-mail integration. DesKilo sends no e-mail of its own: the only e-mails are the account e-mails of the server's sign-in service (sign-up confirmation, password reset, a sign-in code). Everything else reaches a person in the app: the events feed and the bell, push when Firebase is configured, and, in the installed app (not in the browser), a local check-in reminder 15 minutes before a booking (the reminder).
An instance is a whole DesKilo on its own database. Two workspaces — even a development and production pair — share one; two instances share nothing. Use one where personal data or payment credentials must be physically separated, or where a client insists on their own database.
The bundle is the instance's build material: every migration in order, the edge functions, the storage buckets and the seed. It is regenerated whenever a migration is applied, so the bundle and the live database are never out of step.
Create an instance from the app's wizard or from
dart run tool/instance.dart; both apply the bundle to an empty
database and stamp which migration it stands at. A later migration
reaches an existing instance the same way — applied in order from the
stamp forward, never re-run.
The version is a number the schema carries itself:
select public.deskilo_schema_version(); answers the last migration
applied, and every migration writes it in its own transaction (#1312).
An app older than its server keeps working. An app newer than its
server stops on This server needs an update — whoever runs the server
applies the missing migrations; everyone else can still open the Server
screen from there and point the device elsewhere. Without a network the
check simply does not happen, and nothing is blocked.
Configuration and master data travel between instances through the space file, since a deployment needs one database and an instance is the point at which there are two.
Settings → Developer. A ring buffer of the last 500 entries, backed by
a file on the device, with every framework and platform error hooked
into it from the first line of main().
Three shapes, and the middle one is the useful one:
- step — a decision or a server round trip that went as intended.
- refused — the app declining to do the thing someone reached for. Warn level, and the first thing to scroll to when the report is "I tapped it and nothing happened".
- failed — an exception, carrying the same fields as the step that was attempting it, so a red line is never orphaned from its context.
Every line is a verb followed by key=value pairs, so a trace can be
grepped: grep 'act=check-in' reads one kind of attempt end to end, and
grep 'server=' reads every refusal the server issued, with its code,
message, details and hint in one field.
A trace is per device. The log that answers "a member could not check in" is on that member's phone. Export writes it to a file stamped with the app version and the workspace, which is what makes an exported trace tie back to the report it answers. Scanned payloads are recorded by shape — scheme, host, which parameters are present, how long — never by value, because an invite code is a secret and a trace is meant to be sent to someone.
A "started" with no matching "done" means the act never came back:
the app was killed, the request never returned, or an await is
hanging. That gap is the finding.
Developers
Administrators
- Configuring the space
- Configurer l'espace 🇫🇷
- Den Bereich einrichten 🇩🇪
- Configurar el espacio 🇪🇸
- Configurare lo spazio 🇮🇹
- The technical side
- Le côté technique 🇫🇷
- Die technische Seite 🇩🇪
- La parte técnica 🇪🇸
- La parte tecnica 🇮🇹
- Environments: dev and prod
- Environnements : dev et prod 🇫🇷
- Umgebungen: Dev und Prod 🇩🇪
- Entornos: dev y prod 🇪🇸
- Ambienti: dev e prod 🇮🇹
Users — on the guide site