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.
01PackLists per activity, from what you own
02FocusA backlog, a todo list and a daily dump
03CalmStart each day with an empty head
Domain-Driven DesignBounded contextsMCP
Who sees what, where your data goes and how it ships
± 22 min readPublished
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.
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.
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.
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.
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.
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.
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 working
In 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.