<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Drytrix</title>
    <description>The latest articles on DEV Community by Drytrix (drytrix).</description>
    <link>https://dev.to/drytrix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14943%2Fde672dba-18a6-4807-93ed-8743f571c6cb.png</url>
      <title>DEV Community: Drytrix</title>
      <link>https://dev.to/drytrix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/drytrix"/>
    <language>en</language>
    <item>
      <title>Adding Peppol e-invoicing (EN 16931) to a self-hosted Flask app</title>
      <dc:creator>Dries Peeters</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:09:02 +0000</pubDate>
      <link>https://dev.to/drytrix/adding-peppol-e-invoicing-en-16931-to-a-self-hosted-flask-app-4fhj</link>
      <guid>https://dev.to/drytrix/adding-peppol-e-invoicing-en-16931-to-a-self-hosted-flask-app-4fhj</guid>
      <description>&lt;p&gt;Since &lt;strong&gt;1 January 2026&lt;/strong&gt;, Belgian businesses have to exchange B2B invoices as structured e-invoices over the &lt;strong&gt;Peppol&lt;/strong&gt; network. A PDF by email no longer counts.&lt;/p&gt;

&lt;p&gt;I maintain &lt;strong&gt;&lt;a href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;TimeTracker&lt;/a&gt;&lt;/strong&gt;, an open-source, self-hosted time tracker that also generates invoices. I'm based in Belgium and so are many of the people it's built for, so the mandate turned a nice-to-have into a must-have.&lt;/p&gt;

&lt;p&gt;This post covers what I learned building it: the standards, the architecture choice that made it workable for a self-hosted app, and the parts that were harder than expected.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/DRYTRIX" rel="noopener noreferrer"&gt;
        DRYTRIX
      &lt;/a&gt; / &lt;a href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;
        TimeTracker
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Self-hosted time tracking &amp;amp; invoicing for freelancers and teams: timers, projects, PDF and Peppol e-invoicing. Open-source Toggl/Clockify alternative.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TimeTracker&lt;/h1&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Professional Time Tracking &amp;amp; Project Management for Teams&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Track time. Manage projects. Generate invoices. All in one place.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/DRYTRIX/TimeTracker#-whats-new" rel="noopener noreferrer"&gt;🆕 What's New&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-download--install" rel="noopener noreferrer"&gt;📥 Download &amp;amp; Install&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-quick-start" rel="noopener noreferrer"&gt;🚀 Quick Start&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-features" rel="noopener noreferrer"&gt;✨ Features&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-screenshots" rel="noopener noreferrer"&gt;📸 Screenshots&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/docs/GETTING_STARTED.md" rel="noopener noreferrer"&gt;📖 Getting Started&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/docs/" rel="noopener noreferrer"&gt;📚 Documentation&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/UNINSTALL.md" rel="noopener noreferrer"&gt;🗑️ Uninstall&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/CHANGELOG.md" rel="noopener noreferrer"&gt;📋 Changelog&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-deployment" rel="noopener noreferrer"&gt;🐳 Deploy&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎯 What is TimeTracker?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;TimeTracker is a &lt;strong&gt;self-hosted, web-based time tracking application&lt;/strong&gt; designed for freelancers, teams, and businesses who need professional time management with complete control over their data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Perfect for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;💼 &lt;strong&gt;Freelancers&lt;/strong&gt; tracking billable hours across multiple clients&lt;/li&gt;
&lt;li&gt;👥 &lt;strong&gt;Small Teams&lt;/strong&gt; managing projects and tracking productivity&lt;/li&gt;
&lt;li&gt;🏢 &lt;strong&gt;Agencies&lt;/strong&gt; needing detailed reporting and client billing&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Privacy-focused organizations&lt;/strong&gt; wanting self-hosted solutions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can &lt;a href="https://timetracker.drytrix.com/support.html" rel="nofollow noopener noreferrer"&gt;support the project and purchase a key&lt;/a&gt; to hide donate prompts in your instance.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📥 Download &amp;amp; Install&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Get started&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Servers, VPS, homelab&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker pull ghcr.io/drytrix/timetracker:latest&lt;/code&gt; or &lt;a href="https://hub.docker.com/r/drytrix/timetracker" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not legal or tax advice.&lt;/strong&gt; I'm a developer, not an accountant. Check the official guidance from the Belgian FPS Finance before you rely on any of this for compliance.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The alphabet soup, untangled
&lt;/h2&gt;

&lt;p&gt;When I started, the terms blurred together. Here's what finally made it click:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it actually is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EN 16931&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The European &lt;em&gt;semantic&lt;/em&gt; standard: which fields an e-invoice must contain (seller, buyer, VAT breakdown, totals, and so on). It defines data, not a file format.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;UBL 2.1&lt;/strong&gt; and &lt;strong&gt;CII&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Two XML &lt;em&gt;syntaxes&lt;/em&gt; that can carry EN 16931 data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peppol BIS Billing 3.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Peppol network's rules on top of EN 16931, expressed in UBL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peppol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;em&gt;transport&lt;/em&gt;: a network where businesses connect through certified &lt;strong&gt;Access Points&lt;/strong&gt; ("4-corner model").&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Factur-X / ZUGFeRD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A &lt;em&gt;hybrid PDF&lt;/em&gt;: a normal human-readable invoice with CII XML embedded inside it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: &lt;strong&gt;Peppol is the pipe, UBL is what goes through it, and Factur-X is a separate format&lt;/strong&gt; that happens to share the same underlying standard. They need different XML (UBL for Peppol, CII for Factur-X), so I ended up generating both.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture decision: don't become an Access Point
&lt;/h2&gt;

&lt;p&gt;To send over Peppol you go through an Access Point. You can, in theory, speak the protocol yourself: look up the receiver in the SML/SMP directory, then send the document over AS4 with the right certificates, signatures and receipts.&lt;/p&gt;

&lt;p&gt;I built an experimental "native" mode that does SML/SMP lookup and AS4 sending. It works for testing, but it lacks WS-Security, digital signatures and receipt handling, and getting certified is a business in itself. For a self-hosted app used by freelancers, that's the wrong layer to own.&lt;/p&gt;

&lt;p&gt;So the &lt;strong&gt;recommended&lt;/strong&gt; path is a &lt;strong&gt;generic HTTP transport&lt;/strong&gt;: TimeTracker builds the UBL and POSTs it, with routing metadata, to an adapter URL. The adapter forwards it to whatever certified Access Point provider the user already has.&lt;/p&gt;

&lt;p&gt;The contract is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"endpoint_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scheme_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"endpoint_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scheme_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##…::2.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"process_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ubl_xml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;?xml version=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;1.0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; …&amp;gt;…&amp;lt;/Invoice&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter returns something like &lt;code&gt;{ "message_id": "…" }&lt;/code&gt;. Any HTTP status of 400 or higher marks the attempt as failed, and every attempt is stored (&lt;code&gt;pending&lt;/code&gt; → &lt;code&gt;sent&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;) so the invoice page can show a full send history.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bridge sidecar
&lt;/h3&gt;

&lt;p&gt;Asking self-hosters to write their own adapter would kill adoption. So the repo ships a small &lt;strong&gt;&lt;code&gt;peppol-bridge&lt;/code&gt;&lt;/strong&gt; service that runs next to the app in Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;peppol-bridge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;peppol_bridge/Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PEPPOL_BRIDGE_AUTH_TOKEN=${PEPPOL_BRIDGE_AUTH_TOKEN:?Set PEPPOL_BRIDGE_AUTH_TOKEN}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PEPPOL_BRIDGE_PROVIDER=einvoice&lt;/span&gt;      &lt;span class="c1"&gt;# or: peppyrus, generic_custom&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EINVOICE_API_KEY=${EINVOICE_API_KEY:?Set EINVOICE_API_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exposes &lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/test&lt;/code&gt; (checks provider credentials) and &lt;code&gt;/send&lt;/code&gt; (the contract above). Provider differences stay inside the bridge. For example, one provider authenticates with an &lt;code&gt;X-Api-Key&lt;/code&gt; header instead of &lt;code&gt;Authorization: Bearer&lt;/code&gt;, and the app never needs to know.&lt;/p&gt;

&lt;p&gt;An admin &lt;strong&gt;setup wizard&lt;/strong&gt; generates this snippet and points the app at &lt;code&gt;http://peppol-bridge:8088/send&lt;/code&gt;. Adding a new provider means adding a preset to the bridge, not changing the invoicing code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Identifiers: small detail, lots of failed sends
&lt;/h2&gt;

&lt;p&gt;Every Peppol party is identified by a &lt;strong&gt;scheme ID&lt;/strong&gt; plus an &lt;strong&gt;endpoint ID&lt;/strong&gt;. For Belgian companies that's typically scheme &lt;code&gt;0208&lt;/code&gt; with the enterprise number as endpoint.&lt;/p&gt;

&lt;p&gt;Two lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate identifiers before sending.&lt;/strong&gt; Both sender and recipient IDs are checked for scheme and format before anything leaves the server. A malformed ID otherwise fails somewhere deep in a provider's API with an unhelpful error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recipients are per client.&lt;/strong&gt; Each client record holds its own endpoint and scheme. The &lt;strong&gt;Send via Peppol&lt;/strong&gt; button only appears when both are present, so users can't try to send to a client who isn't set up.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  "Make all invoices Peppol compliant"
&lt;/h2&gt;

&lt;p&gt;Most users don't want to think about BT-codes. So there's a single admin toggle that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adds the mandatory BIS Billing 3.0 elements, such as &lt;code&gt;InvoiceTypeCode&lt;/code&gt; 380 and a &lt;strong&gt;Buyer reference (BT-10)&lt;/strong&gt;. If the user leaves BT-10 empty it falls back to the project name, then the invoice number;&lt;/li&gt;
&lt;li&gt;shows &lt;strong&gt;warnings on the invoice page&lt;/strong&gt; when required data is missing (company VAT ID, sender endpoint, client endpoint);&lt;/li&gt;
&lt;li&gt;adds a &lt;strong&gt;Download UBL&lt;/strong&gt; button, so users whose accountant handles sending can still hand over a compliant file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The warnings matter as much as the XML. A perfectly generated document still fails if the company VAT ID or the client's endpoint is missing, so surfacing those gaps before the user clicks Send is what makes the feature usable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Factur-X: the hard part was the PDF, not the XML
&lt;/h2&gt;

&lt;p&gt;Factur-X looks simple: attach an XML file to a PDF. In practice, validators are strict about how.&lt;/p&gt;

&lt;p&gt;What the export does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embeds &lt;code&gt;factur-x.xml&lt;/code&gt; (CII, EN 16931 profile) as a PDF &lt;strong&gt;Associated File&lt;/strong&gt; with relationship &lt;code&gt;Data&lt;/code&gt;, MIME type &lt;code&gt;text/xml&lt;/code&gt;, and the Factur-X XMP metadata.&lt;/li&gt;
&lt;li&gt;Optionally normalises the file to &lt;strong&gt;PDF/A-3b&lt;/strong&gt; in the same &lt;code&gt;pikepdf&lt;/code&gt; pass: XMP identification, an sRGB ICC output intent, and the &lt;code&gt;pdfaExtension&lt;/code&gt; schema declaration.&lt;/li&gt;
&lt;li&gt;Embeds &lt;strong&gt;Liberation fonts&lt;/strong&gt; (metric-compatible with Helvetica, Times and Courier). PDF/A validators reject the unembedded base-14 fonts that many PDF generators use by default, and it's an easy thing to miss.&lt;/li&gt;
&lt;li&gt;Can run &lt;strong&gt;veraPDF&lt;/strong&gt; after export to check PDF/A-3b conformance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one rule I'd recommend to anyone doing this: &lt;strong&gt;fail loudly.&lt;/strong&gt; If embedding is enabled and the step fails, the export aborts with an error. It never quietly returns a plain PDF that the user believes is compliant. Pre-export checks also block the download when a seller or buyer country is missing, or when reverse charge (VAT category &lt;code&gt;AE&lt;/code&gt;) is used without a buyer VAT ID.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it doesn't do (yet)
&lt;/h2&gt;

&lt;p&gt;Being clear about limits matters more with compliance features than anywhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sending, not receiving.&lt;/strong&gt; The mandate also requires businesses to &lt;em&gt;receive&lt;/em&gt; e-invoices from suppliers. TimeTracker has no accounts-payable inbox; that half needs another tool or your Access Point provider's portal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An Access Point account is still needed.&lt;/strong&gt; The bridge is an adapter, not network membership.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XRechnung&lt;/strong&gt; (Germany's variant) isn't supported.&lt;/li&gt;
&lt;li&gt;In-app validation checks structure, not the full Peppol Schematron rules. External validators are still worth running before you go live.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaways for other developers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separate "generate the document" from "transport the document."&lt;/strong&gt; A thin adapter contract let me support several providers without touching invoicing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invest in data-quality warnings.&lt;/strong&gt; Correct XML is useless if the VAT ID or endpoint behind it is missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid PDFs are a PDF/A project.&lt;/strong&gt; Budget for fonts, colour profiles and metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail loudly on compliance steps.&lt;/strong&gt; A silent fallback produces non-compliant invoices nobody notices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full setup docs are in the repo: &lt;a href="https://github.com/DRYTRIX/TimeTracker/blob/main/docs/admin/configuration/PEPPOL_EINVOICING.md" rel="noopener noreferrer"&gt;PEPPOL_EINVOICING.md&lt;/a&gt; and &lt;a href="https://github.com/DRYTRIX/TimeTracker/blob/main/docs/admin/configuration/PEPPOL_BRIDGE.md" rel="noopener noreferrer"&gt;PEPPOL_BRIDGE.md&lt;/a&gt;. There's also a plain-language overview at &lt;a href="https://timetracker.drytrix.com/peppol-einvoicing" rel="noopener noreferrer"&gt;timetracker.drytrix.com/peppol-einvoicing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're working on e-invoicing too, especially for another EU country, I'd love to compare notes in the comments. And if this was useful, a ⭐ on &lt;a href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>opensource</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Host Your Own TimeTracker on a Raspberry Pi in 10 Minutes. Updated for 2026.</title>
      <dc:creator>Dries Peeters</dc:creator>
      <pubDate>Wed, 01 Oct 2025 12:19:23 +0000</pubDate>
      <link>https://dev.to/drytrix/host-your-own-timetracker-on-a-raspberry-pi-in-10-minutes-340m</link>
      <guid>https://dev.to/drytrix/host-your-own-timetracker-on-a-raspberry-pi-in-10-minutes-340m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Updated September 2026.&lt;/strong&gt; TimeTracker has grown a lot since this guide first went up: invoicing, Peppol e-invoicing, mobile and desktop apps, and a much simpler install. The steps below are current.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're a freelancer, consultant, or part of a small team, you probably track billable hours somewhere. Most popular tools are SaaS, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your time and client data live on someone else's server,&lt;/li&gt;
&lt;li&gt;you pay per user, every month,&lt;/li&gt;
&lt;li&gt;and the tool can change its pricing or features whenever it wants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;TimeTracker&lt;/a&gt;&lt;/strong&gt; is an open-source (GPL-3.0), self-hosted alternative. In this guide you'll run it on a Raspberry Pi in about ten minutes, with no git clone and no config editing beyond one secret key.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/DRYTRIX" rel="noopener noreferrer"&gt;
        DRYTRIX
      &lt;/a&gt; / &lt;a href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;
        TimeTracker
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Self-hosted time tracking &amp;amp; invoicing for freelancers and teams: timers, projects, PDF and Peppol e-invoicing. Open-source Toggl/Clockify alternative.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TimeTracker&lt;/h1&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Professional Time Tracking &amp;amp; Project Management for Teams&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Track time. Manage projects. Generate invoices. All in one place.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/DRYTRIX/TimeTracker#-whats-new" rel="noopener noreferrer"&gt;🆕 What's New&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-download--install" rel="noopener noreferrer"&gt;📥 Download &amp;amp; Install&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-quick-start" rel="noopener noreferrer"&gt;🚀 Quick Start&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-features" rel="noopener noreferrer"&gt;✨ Features&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-screenshots" rel="noopener noreferrer"&gt;📸 Screenshots&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/docs/GETTING_STARTED.md" rel="noopener noreferrer"&gt;📖 Getting Started&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/docs/" rel="noopener noreferrer"&gt;📚 Documentation&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/UNINSTALL.md" rel="noopener noreferrer"&gt;🗑️ Uninstall&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker/CHANGELOG.md" rel="noopener noreferrer"&gt;📋 Changelog&lt;/a&gt; • &lt;a href="https://github.com/DRYTRIX/TimeTracker#-deployment" rel="noopener noreferrer"&gt;🐳 Deploy&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎯 What is TimeTracker?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;TimeTracker is a &lt;strong&gt;self-hosted, web-based time tracking application&lt;/strong&gt; designed for freelancers, teams, and businesses who need professional time management with complete control over their data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Perfect for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;💼 &lt;strong&gt;Freelancers&lt;/strong&gt; tracking billable hours across multiple clients&lt;/li&gt;
&lt;li&gt;👥 &lt;strong&gt;Small Teams&lt;/strong&gt; managing projects and tracking productivity&lt;/li&gt;
&lt;li&gt;🏢 &lt;strong&gt;Agencies&lt;/strong&gt; needing detailed reporting and client billing&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Privacy-focused organizations&lt;/strong&gt; wanting self-hosted solutions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can &lt;a href="https://timetracker.drytrix.com/support.html" rel="nofollow noopener noreferrer"&gt;support the project and purchase a key&lt;/a&gt; to hide donate prompts in your instance.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📥 Download &amp;amp; Install&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Get started&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Servers, VPS, homelab&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker pull ghcr.io/drytrix/timetracker:latest&lt;/code&gt; or &lt;a href="https://hub.docker.com/r/drytrix/timetracker" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timers that run on the server&lt;/strong&gt;, so they keep going when you close the browser or your laptop sleeps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Projects, clients, tasks and a Kanban board.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoices generated from tracked time&lt;/strong&gt;, exported as PDF, with multi-currency and tax support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peppol and Factur-X/ZUGFeRD e-invoicing (EN 16931)&lt;/strong&gt;, which matters if you invoice businesses in Belgium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports and CSV export&lt;/strong&gt;, plus expenses and mileage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-user with roles&lt;/strong&gt;, OIDC/SSO and audit logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A web app, a Chromium browser extension, an Android app and a desktop app&lt;/strong&gt; (Windows, macOS, Linux), all talking to your own server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want to look around first? There's a live demo, no signup: &lt;strong&gt;&lt;a href="https://timetracker-demo.drytrix.com" rel="noopener noreferrer"&gt;https://timetracker-demo.drytrix.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Raspberry Pi 4 with 2 GB RAM or more&lt;/strong&gt; (a Pi 5 works too). Use a 64-bit OS; the images are published for arm64.&lt;/li&gt;
&lt;li&gt;Raspberry Pi OS (64-bit) or another Debian-based distro, with network access.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Install Docker
&lt;/h2&gt;

&lt;p&gt;If Docker isn't installed yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log out and back in (or reboot) so your user can run Docker. The install script includes the &lt;code&gt;docker compose&lt;/code&gt; plugin, so you don't need the separate &lt;code&gt;docker-compose&lt;/code&gt; package anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Download the compose file
&lt;/h2&gt;

&lt;p&gt;TimeTracker ships a single-file stack meant for NAS boxes and small servers. It pulls the published image and a PostgreSQL database, so there's nothing to build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/timetracker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/timetracker
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; docker-compose.yml &lt;span class="se"&gt;\&lt;/span&gt;
  https://raw.githubusercontent.com/DRYTRIX/TimeTracker/main/docker-compose.nas.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 — Set a secret key
&lt;/h2&gt;

&lt;p&gt;The stack refuses to start without a &lt;code&gt;SECRET_KEY&lt;/code&gt;. Generate one and save it in a &lt;code&gt;.env&lt;/code&gt; file next to the compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SECRET_KEY=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional: the defaults are &lt;code&gt;TZ=Europe/Brussels&lt;/code&gt; and &lt;code&gt;CURRENCY=EUR&lt;/code&gt;. Change them in the same file if you need to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"TZ=Europe/Amsterdam"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .env
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"CURRENCY=USD"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4 — Start it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first start takes a minute or two on a Pi while PostgreSQL initialises and migrations run. Check progress with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5 — Open it
&lt;/h2&gt;

&lt;p&gt;In a browser on your network, go to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;raspberry-pi-ip&amp;gt;:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log in with the username &lt;strong&gt;&lt;code&gt;admin&lt;/code&gt;&lt;/strong&gt;. That's the default admin name set by &lt;code&gt;ADMIN_USERNAMES&lt;/code&gt; in the compose file; the first login creates the account.&lt;/p&gt;

&lt;p&gt;That's it. You're tracking time. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  First things to set up
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a client and a project&lt;/strong&gt;, and set an hourly rate, so timers aren't free-floating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start a timer&lt;/strong&gt; from the dashboard. Close the tab, come back later, and it's still running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate an invoice&lt;/strong&gt; from the tracked time and export the PDF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install the browser extension, desktop app or Android app&lt;/strong&gt; from &lt;a href="https://github.com/DRYTRIX/TimeTracker/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt; and point it at your Pi's address.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Before you rely on it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access from outside your home network:&lt;/strong&gt; put it behind a reverse proxy with HTTPS (Caddy, nginx or Traefik), or reach it over a VPN like Tailscale or WireGuard. The compose file uses HTTP-friendly cookie defaults for LAN use; switch those to secure settings when you add HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups:&lt;/strong&gt; your data lives in Docker volumes (&lt;code&gt;db_data&lt;/code&gt; for the database). Back them up, for example with a nightly &lt;code&gt;pg_dump&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates:&lt;/strong&gt; pull the latest image and restart:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker compose pull &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SD cards wear out.&lt;/strong&gt; For anything you depend on, boot the Pi from an SSD.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Coming from Toggl, Harvest or Clockify?
&lt;/h2&gt;

&lt;p&gt;TimeTracker can import from the &lt;strong&gt;Toggl Track and Harvest APIs&lt;/strong&gt; directly, and from &lt;strong&gt;CSV&lt;/strong&gt; for everything else. Open &lt;em&gt;Import/Export&lt;/em&gt; from the user menu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;&lt;a href="https://github.com/DRYTRIX/TimeTracker" rel="noopener noreferrer"&gt;Star TimeTracker on GitHub&lt;/a&gt;&lt;/strong&gt; if it's useful. It helps other people find it.&lt;/li&gt;
&lt;li&gt;🌐 Website and comparisons: &lt;a href="https://timetracker.drytrix.com" rel="noopener noreferrer"&gt;https://timetracker.drytrix.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐛 Questions or bugs: &lt;a href="https://github.com/DRYTRIX/TimeTracker/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt; or &lt;a href="https://github.com/DRYTRIX/TimeTracker/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What are you running on your Pi? I'm curious what else people keep next to their time tracker. 👇&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>python</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
