Architecture & platform

Pack a Thing

On this page
  1. What it is, in short
  2. Why I build it
  3. Your data stays yours
  4. Alpha, beta and stable
  5. A roadmap to vote on
  6. A waiting list
  7. From merge to production
  8. Where the architecture shows up
  9. The tool is not the point

Architecture & platform · MVP

It started with packing. I am going on a trip and taking things with me: where are my things, how do I pack them, and which packing lists help? Pack a Thing gives you lists per activity and an inventory of what you own. The ambition is lists that learn from your behaviour, your activities and your inventory; that part is still to come. Alongside packing sits focus, a backlog plus a todo list and a daily dump, so you can start each day with an empty head. I build it to make my own days calmer. It is live in an MVP state, with most of its powerful functionality still behind the beta and alpha channels, at packathing.nl. How I build it, and the architecture behind that, is its own article: let software emerge.

  1. 01 Pack Lists per activity, from what you own
  2. 02 Focus A backlog, a todo list and a daily dump
  3. 03 Calm Start each day with an empty head
Domain-Driven DesignBounded contextsMCP

Who sees what, where your data goes and how it ships

± 22 min read Published

Executive summary

  • What. Pack a Thing started as a packing app: lists per activity and an inventory of what you own, with lists that should learn from you over time. Focus (a backlog, a todo list and a daily dump) supports it.
  • Why. To make my own days calmer so I can flow more, and to help others do the same. The goal is that you need the app a little less each year.
  • Data. Personal data has its own audited boundary, a data governance capability decides how long it is kept, error reports are scrubbed, and AI cannot reach your data unless you connect an assistant yourself, which is always your own choice.
  • Channels. Alpha, beta and stable are a ladder of who sees what, on one commit and one database, not three versions of the code.
  • Roadmap. A clear roadmap on which users vote and give feedback early, which shows me the demand and what to build first.
  • Waiting list. Invite-only on purpose, so growth is phased and the infrastructure can be scaled or refactored in time.
  • Release. A train with a check at every stop: a fast gate on every pull request, the full suite and a browser smoke on UAT, and a release that is a deliberate, approved act which can be rolled back.
  • How it is built. Play, build, rest, refactor, with Domain-Driven Design and contracts between capabilities. That is its own article: let software emerge.

What it is, in short

It started with packing: I am going on a trip and taking things with me. Where are my things, how do I pack them, which packing lists help? So Pack a Thing is lists per activity and an inventory of what you own, with the ambition that the lists learn from your behaviour, your activities and your inventory. Alongside it sits focus, which supports packing: a backlog of rocks, pebbles and sand, a todo list for what has a date, and a daily dump that catches whatever is in your head. A day starts empty, and what you do not get to stays where it belongs instead of piling up behind you.

The product itself, the problem it tries to solve and why, is its own story, told in the app at packathing.nl/about. How it is designed and built, the way of working and the architecture, is its own article: let software emerge. This one is about who sees what, where your data goes and how a change reaches production.

Why I build it

First of all to make my own life easier, so that I can flow more. After that, to help and inspire other people in the same thing. Not to make anyone dependent on the technology: the tool is there to create calm and focus, and the real goal is that new cognitive paths get worn in, so that you learn to do this process yourself, better and better, without the app. The way I work towards that, play first and refactor on purpose, is in play, build, rest, refactor.

Your data stays yours

An app that keeps a list of what you own, what you are working toward and what is on your mind holds things that are nobody else's business. So the question of who can reach that data is part of the architecture, not a footnote to it.

Personal data has its own boundary

Personal data, today the email address, is kept in its own place inside Identity & Access, apart from the rest of the account. The address is stored encrypted on a key of its own and is found through a blind index, so there is no plain column left to query. Nothing shows it by default: an administrator sees that an address exists, not the address. Reading it is a separate, explicit action, and every reading is written down (who, whose, from where) and removed again once the retention period has passed. Backups are encrypted before they leave the server. A weekly audit looks at where every third party that touches user data keeps it, and the Terms say so plainly in a section called "Where your data lives".

That is where it stands today: a boundary in the code and in its own table. The design goes further, and that is the direction it is built in. Personal data lives physically apart, in its own database or service, so there is no route to it except through the front door, not for an API token, not for a query and not for the app’s own code. The front door is also where logging and auditing live. Every reading passes it, so the record of who read what is guaranteed by design instead of by convention: nobody has to remember to write it, because there is no way in that skips it.

The people who run the console are often users of the product themselves. So a console role is not simply there when you are signed in: whenever someone wants to use one of those roles, an operator or an administrator for example, they have to authenticate again. That guards against an open session in a browser being abused by someone else to reach those roles.

That last part is a choice and not an accident. I do not promise that all data stays inside the EU/EEA, because a promise like that would make the app much harder to build, especially during early-stage MVP development, and would tempt me to blur the truth when a vendor turned out not to qualify. I allow parties outside the EU/EEA, the email service is a US company under the EU-US Data Privacy Framework for example, and I am transparent about it in the Terms and in the app. The reasoning behind that is in the article on transparency over perfection.

Transparency you can read

A small example is Gravatar. It is used in a lot of places, and few people realise that when it shows your picture, the party behind it receives your email address (as a one-way hash) and the IP address of whoever is looking at it. In Pack a Thing profile pictures are generated from your name by default, and nothing is sent anywhere. Using your Gravatar photo is an opt-in, and the setting says in plain words what it sends and to whom. The Terms say the same thing in plain words.

Profile photo

By default your avatar is generated from your name — nothing about you is sent anywhere. You can opt in below to use your Gravatar photo instead.

Use my Gravatar photo instead This sends your email address (as a one-way hash) and your IP address to Gravatar — a US company — every time your avatar is shown, for as long as this is on.

Terms · Where your data lives

Profile pictures are generated by us by default, and nothing is sent anywhere. If you turn on "Use my Gravatar photo instead" in your profile settings, we look it up at Gravatar (Automattic, also a US company under the same framework) — that sends your email address (as a one-way hash) and your IP address to the US, only for as long as that setting stays on.

The setting says what it sends and to whom, and the Terms say the same thing in plain words. (An illustration of both, not a screenshot, so it cannot go out of date.)

Transparency only works if people can actually read what is being disclosed, and honestly, who reads long walls of small print and legal jargon? So the Terms are written to be read. They are split into short sections with plain titles, such as "No cookies", "We never sell your data", "No built-in AI access to your data" and "Where your data lives". Every section keeps track of when it last changed, a section updated since you last accepted is marked "Updated" on the page, and when something does change, the page that asks you to accept again lists exactly which sections were updated.

There is a rule behind that too. When something is taken out of the Terms, such as a vendor that is dropped, nobody has to accept again, because it only improves their position. When something is added, such as a new processor, everyone does, because it changes who handles their data.

Data governance: how long, and who did what

Data leaks keep making the news, and the GDPR asks you to know where personal data lives and how long it stays. So that question has a capability of its own, Data governance, next to Identity & Access. It does three things.

Retention. Every capability that keeps something that should expire, a log or a trash for example, registers a retention policy of its own and says how long it keeps rows and what “gone” means for them. One nightly job walks all the policies. Data governance only decides when, so it never has to know what is being kept. Even finished queue work is on that list, for a reason that is easy to miss: a failed job still holds its payload, and that can contain the address a mail was meant for.

An audit trail. A history of changes to an account, kept apart from the current value so that value can stay a plain row. A row says what changed, about whom, by whom and from where. It is never changed or removed through the application, it goes when the account it is about does, and it is read in the console.

Scrubbing. What leaves the app for a log or an error service passes the same scrubber first, which is the next part.

Error reports are scrubbed first

Errors are reported to a monitoring service so a bug gets fixed before anyone has to report it. Nothing is sent with the user attached, and before an error leaves the app, email addresses, IP addresses and identifiers in the text are replaced by placeholders. A pattern only catches a pattern: free text that someone typed cannot be found this way, so the real answer there is to send less, not to write a fourth rule.

AI cannot reach your data on its own

Pack a Thing is built by AI agents, and that is the question people ask first: can the AI see my lists? It cannot. The agents work on code, issues and the scrubbed error reports. The one role that touches production, the Operator, does so for incidents and releases, and has a written mandate like every other role.

There is exactly one way for an AI to get to your data, and it is yours to open. You can connect an assistant of your own to your account through MCP, so it can read your lists, add to your backlog or plan your day. That is very powerful, and it stays a choice you make. It acts as you and as nobody else, within the permissions you grant, and you can see every connected app on your profile and end the connection whenever you want.

Who can reach the data of a user, and who cannot Your data lists, inventory, backlog, day email apart, encrypted The AI builders work on code, not on your data no route Error monitoring scrubbed before it leaves the app emails, IPs, IDs removed Your own assistant only if you connect it yourself your choice, scoped, revocable You see and end every connection
Routes to a user's data. The dashed lines are routes that do not exist; the solid one is the only way in, and the user opens and closes it.

Alpha, beta and stable

Pack a Thing has three release channels, and they are about who gets to see what, not about different code. Stable is for everyone. Beta is for people who want to use what has just been finished, and alpha is for people who want to look at what is still being built. Each capability carries the stage it is in, and that stage decides who is shown it.

The channels are a ladder: an alpha tester reaches beta as well, and an administrator reaches everything. The ladder decides what you can see one rung wider than what you can use. A beta tester is shown what is being built without being able to open it, because seeing what comes next is the point of being on the ladder. Below that it is not teased at all. A person switches channel from a menu, and what applies is the lower of the choice and the rights, so a role that is handed back never leaves anyone stranded: they fall to the highest channel still open to them.

This is also why the live app can look small. Pack a Thing is in an early phase and live in an MVP state, and most of its powerful functionality is still behind the beta and alpha channels, waiting for its turn.

Three release channels as a ladder, all on one commit and one database Stable everyone finished software Beta beta testers capabilities that reached beta Alpha alpha testers capabilities still being built An alpha tester also reaches beta; a lost role falls to the highest channel still allowed. one commit · one database · one release
The channels differ in who may see what, not in the code. What applies to a person is the lower of the channel they choose and the rights they have.

The three channels share one commit and one database, and that is a decision with a price. A release cannot be tried out on alpha first and only then given to everyone, so the safety has to come from the train below and from changes that only ever add to the schema.

A roadmap people can vote on

What is coming is not kept to myself. There is a clear roadmap, grouped by capability, that shows what has shipped, what is in beta, what is being built, what is next and what is parked for later. Users can vote on it and leave feedback while an idea is still early, which is exactly when it is cheapest to change. More on that in early feedback and releasing without fear.

That helps me in two ways. It helps me understand the domain and the question behind it better, through the eyes of other users and not only my own. And it helps me decide what is relevant to build first. A vote is a signal and not an order, but it is a far better signal than my own guess.

A few details are deliberate. Everyone gets a fixed number of active votes to spend at once, so a vote costs something and means something. Open items are ranked by their votes. The roadmap is also built the way the rest of the app is: each capability keeps its own list of what is coming and registers it with the roadmap, and the roadmap never needs to know what any of them contain. Once something is done it moves into a changelog written in plain words, for the people on stable, so the roadmap and the changelog never overlap.

A waiting list, so growth stays phased

Pack a Thing is invite-only for now, on purpose. There are two ways in, and they meet quickly: a member invites someone, or someone without an account asks to be let in on the waiting list. Both are invitations, both wait, and a single yes sends the signed link that opens the account. An invitation from a member sorts to the top, so someone who was vouched for goes first.

The point is to grow in phases. A waiting list lets me decide how fast new people arrive, so I can scale the infrastructure, or refactor it for scale, in time instead of in an incident. It is the same idea as the rest of the app: the pace is a choice, made in advance, and not something that happens to me.

It has a privacy side as well. Both forms answer identically whatever the address turns out to be, a newcomer, a member or someone already waiting, because anything else turns the form into a way to find out who is a member, one address at a time.

From merge to production

None of this counts if a change cannot reach production without anyone holding their breath. So the way there is a train with a check at every stop, and each stop checks something the previous one cannot. The idea behind it is in releasing without fear.

Every pull request gets a preview site of its own, with its own database, so a change can be looked at before it is merged. The pull request gate runs on every push and is kept fast on purpose: the architecture check that guards the boundaries between capabilities, static analysis, the unit tests and the tests of the domain layer, which touch no database and render nothing. A newer push cancels the older run.

After the merge nothing deploys by itself. UAT is deployed on a trigger, or once each weekday morning when main has moved, and it is where a candidate is vetted: the full test suite on a database of its own, followed by a browser smoke. That run opens every page and fails on any uncaught script error, then walks the interactions that only exist in the browser.

A release is a deliberate act. It starts by refusing three things: another release still running, a commit the full suite has not passed, and a UAT that does not answer. Then it waits for my approval. After that the jobs stack goes first, because it is the one that migrates the database, with a backup taken before. The web stack follows, the run waits until production reports that the commit is live and the migrations are complete, and a smoke signs in as test accounts and visits every entry page. The commit is then tagged, and a rollback is simply a release of an earlier tag.

The release train: pull request, main, UAT, release and production, with the checks at each stop Pull request own preview site Main one trunk UAT own database Release started by hand Production after approval PR gate ✓ architecture check✓ static analysis✓ unit and domain tests ✓ nothing deploys by itself UAT gate ✓ full test suite✓ browser smoke Prepare ✓ suite green on this commit✓ UAT healthy✓ no other release running Approval ✓ migrations, backup first✓ web stack✓ smoke on production A red check stops the train at that stop. A rollback is a release of an earlier tag.
From a pull request to production. Each stop checks something the previous one could not, and nothing moves on by itself after the pull request is merged.

Because the three channels share one commit and one database, there is a single release to production. That is also why migrations and job payloads only expand: the old code and the new schema are expected to meet for a while.

Two rules about the tests themselves. A test may not depend on the clock, because one that goes red depending on the time of day blocks a release for a reason that has nothing to do with the change. And I try to be honest about what a green pull request does not prove: the HTTP and database tests only run before UAT, so for every written rule a generated page lists which guards run on the pull request gate and which only before UAT. A rule whose guards are all UAT-only can be broken by a green pull request, and that is worth being able to see.

Where the architecture shows up

The way of working and the architecture behind Pack a Thing are told in let software emerge, with a fictional webshop as the example. This is where they land in the real app.

The capabilities are Packing, Focus, Portal and Core, and they do not reach into each other, which is the idea of contracts owned by the one that asks. A backlog holds nothing on a day, and the todo list is nothing but days, so the backlog asks for an entry to be put on one. A row on a packing list can become a task on Todo, such as “charge your batteries a few days before you leave”. The activity asks and Todo answers. If you cancel your Todo subscription, the task stays on the activity as a plain item to tick off, without a date.

Dependencies between the four product groups, all pointing one way 746 638 76 33 Packing activities, inventory Focus backlog, todo, daily Portal accounts, privacy, console Core shared plumbing
Product groups and the number of code references between them. Arrows only point towards what is more general; a tool checks that on every change, so a circle cannot slip in unnoticed.

Underneath, the same boundaries show up in the data. Packing and focus each own their tables and hang off the user, and where they meet, one side asks and the other owns the answer.

Sketch of the data model: packing and focus both hang off the user, and the email address sits apart PACKING FOCUS users data_governance_emails encrypted, in its own boundary activities nests: trips activity_checklist_items inventory_nodes nests: cupboards focus_backlog_entries nests: a tree focus_backlog_todo_links todo_tasks focus_dump_items
A sketch of the data model, not the full schema. Both groups hang off the user and meet at one seam, a checklist row that becomes a task on a day; the email address lives in its own table, apart from the account.

Pack a Thing is built by an organisation of AI agents with an org chart, with the same roles and mandates a team of people would have, as described in the same shape around who builds it. A product owner and an enterprise architect refine an item together, a scrum master sizes it, and engineers pick it up from there. In a bigger organisation the enterprise architect is a strategic role and the application architect works in the code; in Pack a Thing the enterprise architect does both. Nothing gets picked up that has not been refined and sized first, and each step is a handover to a different role.

Organisation chart of the AI roles that build Pack a Thing MT (owner) Engineering Manager Product Owner Enterprise Architect Data Residency Officer Support Scrum Master Operator Application Architect Frontend / UI-UX Engineer builds the code keeps it running, safe and in the EU/EEA decides what and how
Who dispatches to whom. Every role has a mandate and explicit restrictions; work moves between them through defined handovers.

A build philosophy is only worth reading if it holds up in a real codebase, so here is where each part of it lands.

From the way of workingIn the codebase
Refactoring as a fixed step, not a someday A working agreement that puts bugs first, then stabilisation and tech debt, then new functionality — strictly in that order
Boundaries you can read Every capability in its own directory with a README and a descriptor; an ADR stating that directories follow boundaries before types
Core, supporting and generic subdomains A subdomain type per capability, visible on the roadmap rather than buried in a document
Guarding boundaries instead of remembering them Static analysis over the dependencies between capabilities, with ADRs for who owns a contract and for discovering rather than enumerating
No circles between capabilities An ADR against them and an architect, the enterprise architect who here also does the application architect's job, who guards it: a violation may appear while playing and building, and is refactored out afterwards
Who owns which fact A backlog that holds nothing on a day and a todo list that is nothing but days, with the move across that seam going one way
Letting it rest and gathering data An insights capability and OKR metrics, a feedback inbox that feeds the next step, and live monitoring on the error logs
Structure around the builders too An org chart of roles and teams, modelled as AI agents, with explicit mandate and restrictions: product owner and enterprise architect refine, scrum master sizes, engineers build
Not too much structure either A deliberately short ADR folder: a long list of ADRs is a list nobody reads
Refactoring safely while in production Expand-only migrations and job payloads: a release may add, never remove, and the old shape is dropped only once nothing reads it

The tool is not the point

If Pack a Thing works the way it should, you need it a little less each year. The lists, the inventory, the backlog and the dump are scaffolding for a habit, and the habit is the product. The same goes for the architecture: the boundaries are there so that the next change is easy, not so that the diagram looks impressive. A river needs a bed, and then it is the river you came for, as the article on architecture puts it.

The sync with Google Calendar and Tasks is there for the same reason. The day itself should be something you walk through with as little interaction with the app as possible. A plan you have to go back in and manage is just another list.

Pack a Thing is live at packathing.nl, still in its early phase and in an MVP state. Most of its powerful functionality is hidden behind the beta and alpha channels for now.