smooth-app
A collaborative, free and open database of ingredients, nutrition facts and information on food products from around the world
Efferent encrypts every day of Apple Health on your iPhone and sends it to an archive your AI agent can read for you. No account, no subscription, source on GitHub.
HealthSwift · PythonGitHub @korchasaOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
iPhone app that mirrors Apple Health into an end-to-end encrypted archive you host yourself
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
An iPhone app that sends your Health data to a server you own, so that software which is not an
Apple device can read it.
The name comes from the efferent nerves — the ones carrying signals away from the centre. That is
most of the design in one word: nothing ever connects to the phone. It sends its days out, and when
an agent has something to write into Health it collects that too, on its own schedule.Read the full README ↗An iPhone app that sends your Health data to a server you own, so that software which is not an Apple device can read it.
The name comes from the efferent nerves — the ones carrying signals away from the centre. That is most of the design in one word: nothing ever connects to the phone. It sends its days out, and when an agent has something to write into Health it collects that too, on its own schedule.
iOS cannot be a server. The app is suspended seconds after it goes to the background, a listening socket is closed by the system, the address changes with the network, and behind NAT the phone is unreachable anyway. So nothing ever connects to the phone. The phone pushes, and the meeting point with whoever reads the data lives elsewhere.
One decision follows from that and explains most of the code: the unit of everything is a day.
The phone does not work out what changed and send a difference. It notices which dates were touched, re-reads those days out of Health in full, and puts each one up as a single object named by its date. Writing a day again replaces it. So:
Not a copy of Health. HealthKit is already the source of truth and sits a millisecond away; mirroring it would cost hundreds of megabytes and a migration every time a record changes shape, and buy nothing. What survives a relaunch is bookkeeping:
The fingerprint is what keeps re-reading cheap: a day rebuilt from Health that comes out byte for byte as it was sent is not uploaded at all.
The row per record exists for one reason. When something is deleted, HealthKit hands over an identifier and nothing else — no date, no type — and the record it names is already gone, so nothing can be asked about it afterwards. Writing down its day while it still existed is the only way to know which day to rebuild without it.
Days and the anchor are marked in a single transaction. If the anchor could move without them, HealthKit would consider that data delivered and never offer it again — data loss with no error anywhere.
A fingerprint is not a fact about Health. It is a claim about the archive — "it already holds exactly this" — and nothing on the phone could ever test it. So an archive that lost a day left the device certain of something untrue, and certain of it for good: the day is re-read, comes out identical, matches the fingerprint, and is never sent again. Silently, for history nobody is looking at. That is not a hypothetical: deleting objects from the bucket does exactly this, and afterwards every counter on the phone still reads zero pending.
So a pass now begins by reading the listing and comparing it against the days that ought to exist — the first day Health knows about through today, not the days the phone believes it sent, because the ledger is the thing under suspicion. Whatever the archive does not have loses its fingerprint and goes again in that same pass. Sending was already idempotent — a day is written whole and replaced whole — so re-sending one costs bytes and nothing else, and there is no state anywhere that a duplicate could corrupt.
It is cheap enough to do without thinking about it, but not free, so it runs at most once a day. A page is a thousand days, which makes a decade three round trips and about two hundred kilobytes — under two seconds. The failure behaviour is the part that matters: a walk that cannot finish throws rather than answering with the pages that did arrive, because a partial listing would name the rest of the archive as lost and the phone would dutifully re-upload years. A check that fails neither stops the send behind it nor counts as done, so one bad moment does not buy a whole day of not looking.
Two streams, because one would be wrong.
Totals, bucketed by hour and by day: steps, walking and running distance, active and basal
energy, flights climbed, exercise and stand minutes. These come from HKStatisticsCollectionQuery,
never from adding samples up — iPhone, Watch and other apps all write steps for the same minutes,
and summing them double-counts against what the Health app shows.
Records, kept as they are: sleep, workouts, heart rate, heart rate variability, resting heart rate, respiratory rate, blood oxygen. A total of any of these would say nothing. Sleep in particular arrives as overlapping stretches with stages rather than one interval per night; stitching them into "a night" is a judgement call and is left to whoever reads the data.
A record belongs to the day it started on. A night that begins before midnight is in the evening's day, which is what a person means by "that night" — and it keeps a record in exactly one day, which a day written whole requires.
The last seven days are re-read on every refresh, because the Watch syncs late and yesterday can still grow tomorrow. Almost all of it costs nothing: a day that did not change never leaves the device.
The first export finds how far back Health goes and marks every day since. That part takes a second, because marking a day is a row and nothing more; the sending that follows needs nobody watching, and resumes on its own, because a day is either in the archive or still marked. Hourly totals begin the day the app was installed — buckets by the hour for a decade would be several hundred thousand readings at a resolution nobody asks of last decade.
Days are the person's own days. The time zone comes from the device; the numbering is always
Gregorian, because Calendar.current follows the phone's region and can be Japanese or Buddhist,
where the year is not 2026 and nobody else could name the day.
A day is a set of columns, in one JSON object:
{"series":[{"d":[0,0],"k":"hk","metric":"heartRate","source":"Apple Watch","t":[0,42],
"t0":1754557200,"unit":"count/min","value":[60,61]}],"v":2}
Rows that agree on kind, metric, bucket, unit and source share a series and say all of that once.
t0 is the first instant in whole seconds since 1970, t counts each row from the one before it,
d is how long each row lasted, and a column is written only when some row in the series has that
field. v is the layout, so an old reader can refuse a shape it does not understand instead of
quietly parsing nonsense. A total names its bucket; a record does not, and that is the only
difference between the two — there is no kind on an event beyond k, because the third one a kind
used to be for was a deletion, and a day sent whole says a record is gone by not containing it.
A reader unpacks that back into one object per line, exactly as it always was:
{"id":"agg:steps:2026-08-07T09:00:00Z:h","v":1,"metric":"steps","bucket":"hour","value":842,"unit":"count"}
id is not on the wire. It is rebuilt from the kind, the metric and the instant, because the
HealthKit record id it used to carry was 58% of everything this app uploaded and no reader ever
looked at one. Two records can begin in the same second — two sleep stages, or one heartbeat seen by
two devices — so rows that land on one identity are numbered #1, #2 in the order the day holds
them. Over a decade of real days that is 3 620 events out of 994 336.
The whole decade is 4 MiB compressed instead of 34.
The bytes have to come out the same twice or the design falls over: the body is what decides whether a day has changed since it was last sent, and HealthKit does not promise to hand records back in the same order twice. Sorting by id used to do that. Now the order is over the content itself — series by their shared fields, rows by their instants and then their values — and tests on both sides shuffle real days to prove it.
Instants are whole seconds. The line format said the same in ISO-8601 and truncated just as quietly; here it is the shape of the field.
Everything, for good. The service is not a letterbox that empties when the reader collects — it is where the history lives, so an agent can ask a question months later without the phone being awake, reachable, or still owned by the same person.
PUT /b/<bucket> creates the logical archive with an empty signed request. It records only
the phone's public signing key, before there is a Health day to upload.PUT /b/<bucket>/days stores the days the request carries, each replacing what was there.
Every date is in what was signed, so a stored day cannot be passed off as another date by anyone
in between. The answer names the days that landed, and that is what lets the phone stop marking
them; a day it does not name simply goes again.GET /b/<bucket>/days?from=…&to=… names the days in a range, with the size of each and when
it was last written. Both ends are inclusive; both are optional. It pages, and next has to be
followed until it comes back null — a listing that stopped at its first page would report the rest
of a decade as nothing at all, and would do it without an error.GET /b/<bucket>/d/<day> hands that day back, exactly as it went in.GET /b/<bucket>/d?from=…&to=… hands a range of days back in one answer, in the frame an
upload travels in: per day its date, its length and its sealed bytes, oldest first. Both ends are
inclusive. At most 92 days and 8 MiB go at once, decided from the sizes the listing already gives;
x-efferent-next names the last day sent when more remain, and it goes back as after. "The last
three months" is the commonest question asked of history, and it costs one request rather than
ninety-two.GET /b/<bucket>/stats says how much is there — days, bytes, first and last — without handing
any of it over. It is encrypted anyway; this is for deciding whether to fetch.PUT /b/<bucket>/reader records, signed by the phone's writer key, the public half of the
read key described under "Who can read it". From then on the two listings, a day, a range and an
outcome answer only a read signed with that key; stats stays open./mcp/b/<bucket> exposes the keyless remote MCP tools for archive metadata and ciphertext
links plus setup_guide, which returns the complete local Python reference without accepting any
arguments, and list_edits, which says what is waiting for the phone. It never accepts a key, so
once a read key is registered, the tools that would list or hand back days or edits answer with the
local command that signs the read instead.And the queue in the other direction, which the service holds until the phone has collected it:
PUT /b/<bucket>/editor records, signed by the phone's writer key, the public half of the
editor key the phone made for its agent. Whoever signs with that key may put edits into the queue;
the service refuses everybody else and decides nothing further.POST /b/<bucket>/edits takes one sealed edit — the agent's items, packed, sealed to the
reading key and signed by the editor — and names it. The name is the service's clock in
milliseconds and a random tail, so the queue is in the order it was written to.GET /b/<bucket>/edits lists what is waiting, or with status=all what became of everything:
pending, applied, partial or failed, with counts. It pages like the day listing.GET /b/<bucket>/e/<name> hands one edit back to the phone, exactly as it went in, with the
editor's key, signature and timestamp in headers so the phone can check the signature itself. Only
the phone's writer key may ask; an edit is nobody else's business, ciphertext or not.PUT /b/<bucket>/e/<name>/outcome is the phone saying what it did — how many items landed
and, for each that did not, its index and a word. The service keeps that under o/<name> and
deletes the edit, so the queue is exactly what the phone has not yet answered for.GET /b/<bucket>/o/<name> hands the outcome back to whoever asks.The upload time in a listing is what keeps a mirror in step. A day can be rewritten at any moment, so "everything after where I stopped" is not a question that can be asked any more. "Everything that changed since I looked" is, and it is the same one listing.
Which days exist, how big each is, and when each was written. Not what happened in them, not at what time, not of what kind. A day is one sealed blob and the service has no way inside it.
That is as coarse as it can be while still answering a question: the date is the address, and without it there would be nothing to ask for. Everything finer — which readings, at what hour, of what metric — happens after decryption, on the machine holding the reading key.
Batching adds one thing to that list and it is worth naming rather than glossing over: the service sees which days arrived together. It already knew as much from their write times landing in the same second, so nothing new is given away, but the frame says it outright.
An edit adds to the list in the same coin: that one arrived, how big it was, which editor key signed it, and afterwards how many items the phone applied and the word it gave for each that failed. Never a metric name, never a value, never which day inside the edit changed — the phone re-uploads those days whole, and a rewritten day looks like every other rewritten day.
The wake adds the one line that is not about the archive at all: the service holds a way to ring this phone, and knows the moments it did. That is a device token Apple issued for this install of this app, registered by the phone and deleted when the phone disconnects. The push itself carries nothing — no count, no name, no day, no metric — and shows nothing; it is a moment of being awake, and the phone decides everything that follows. It exists so an agent's edit reaches Health while the app is closed, instead of waiting for whoever is holding the phone to open it. A phone that never registers is slower and works the same.
Encryption hides contents, not the fact of them, and the privacy copy should say so plainly.
Two ways, and they differ in what they cost rather than in what they answer. ask goes to the
service every time and downloads only the days a question covers. sync keeps a local copy — one
file per day — and query then answers from it with the network switched off:
deno task efferent ask --metric sleep --since 2026-08-01 --until 2026-08-31
deno task efferent sync # copy every day that changed since last time
deno task efferent status # what the archive holds, what the mirror has
deno task efferent query --metric sleep --since 2026-08-01
Both fetch one day earlier than asked for. A night that began before midnight is in the evening's day, so a question about the 11th that fetched only the 11th would miss the night it is asking about — silently, which is the worst way for a query to be wrong. The exact filtering happens after decryption, and compares day against day: an event's times are instants and a bound is a date, so comparing the two as strings would put every reading of the 6th after the 6th.
Neither asks for a day at a time. Days close together share one range request of up to 92 days, and four ranges are in flight at once, so a fresh copy of a decade is about 50 requests rather than about 3 920, and the everyday refresh of recent days is one.
sync is safe to interrupt. Each day is its own file and what was taken is written down as it goes,
so a run that stops costs the days it had not reached and nothing else.
Decryption and analysis run on the agent's machine. The connection starts on the phone:
the phone creates the archive and reading key, then hands an otherwise unprepared agent a keyless
remote MCP URL containing the bucket id and the reading key as a separate local secret. The short
instruction tells the agent to call setup_guide first, and says how: the endpoint is streamable
HTTP with no authentication, and one JSON-RPC request fetches the guide when the client cannot add a
server in the running session. The remote MCP server returns setup text,
archive metadata and ciphertext only. The complete decision and the boundary between phone,
Cloudflare and agent are in documents/connection.md.
The Worker exposes the keyless remote MCP at /mcp/b/<bucket-id>. It offers archive metadata and
an argument-free setup_guide tool that returns the exact Python source needed to fetch and decrypt
days locally. The agent saves that source and the handoff in private local files, asks
archive_status which dates the archive spans, and runs the reference once for the range a question
needs: --from and --to fetch it a quarter of days per request and print NDJSON with the day on
every line. The tools that list sealed days and hand out ciphertext links answer only while the
phone has not registered a read key; afterwards they point at the same local command. No repository
checkout, Deno installation, second MCP server, separate prompt URL or gateway restart is part of
the connection.
The part that answers runs next to the reading key, and it has to. The remote MCP endpoint is discovery and ciphertext transport. The embedded script validates that the key belongs to the bucket, fetches only ciphertext, signs every read with the read key it makes from the reading key, and emits local NDJSON. The agent then analyses those records with local code and never sends plaintext or the reading key to a remote tool.
The reading side an agent keeps is reader/efferent.mjs: one file that needs Node 22 and nothing
else, and is at once the command line, the local MCP server (node efferent.mjs mcp) and the
program an agent skill carries. A skill is what makes the archive reachable from every session
rather than the one the handoff was pasted into, and the popular skills count on Node, not on a
Python of a given version. skill/efferent/SKILL.md is that skill; deno task skill puts it and
the client together in build/skill/efferent/. The Python package in reader/ stays as the
reference the phone is checked against, and the setup_guide script is still its part that fetches
and opens a day.
An agent can also put entries into Health — meals as energy, protein, carbohydrates, fat and water;
sleep by stage; body mass — and it does so without anything on the service ever seeing a number.
The phone makes a third key for it, an Ed25519 editor key, registers its public half with the
service and hands the private half over as a fourth field of the same handoff, Editor key:,
shaped efferent-editor-v1.<private>.<public> like the reading key beside it. A handoff from
before writing existed has three fields and stays read-only; a fresh one for the same archive adds
the editor key to an existing reader without touching the rest.
An edit is a list of items. A put adds an entry or replaces the one written earlier under the same
id; a delete removes it. The id is the agent's handle — agent:meal:2026-09-07:lunch — and the
phone turns it into a HealthKit sync identifier with a version it keeps itself, so applying the same
edit twice, after a crash before the phone could say it had, still ends with one sample. Only entries
written this way can be replaced or removed: what the watch, the phone or another app recorded is
Health's and stays as it is, and an item that names one fails with a word rather than being skipped.
The path is the archive's path run backwards. The agent packs the items, seals them to the phone's own reading key with the bucket in the tag, signs the sealed bytes with the editor key and posts them; the service checks the signature, stores the ciphertext and names it. The phone, on any launch that can reach Health and is not paused, lists the queue, fetches each edit with a writer-signed request, checks the editor signature itself — a service that decided on its own what goes into Health would be able to write into Health — opens it, applies the items and reports an outcome: a count of what landed and, per failed item, its index and one of a closed set of words. The service replaces the edit with its outcome, and the days the items touched are marked and re-uploaded whole, so the entry shows up in the archive afterwards like anything logged by hand. An edit whose outcome the service did not accept stays in the queue and is applied again; that is what the version is for.
An edit the phone has answered is never applied again, whoever serves it. A service in working order
never hands one back, because it deletes the edit when it takes the outcome; the check is for one
that does, since an old put applied a second time puts an old value back over the correction that
replaced it. The phone writes down the SHA-256 of each answered edit's sealed bytes, with its name
and the moment its editor signed it, and refuses as replayed an edit whose bytes it has answered
before under any name, or one signed more than 15 minutes before the newest edit it has answered.
Nothing in Health is touched, and the outcome says why. The second rule covers edits answered before
the digests were kept: the service takes an edit only within five minutes of its signature, and the
phone answers the queue in order and stops at the first edit it cannot answer, so a genuine edit is
never signed that long before one answered ahead of it.
Every item lands, and every item can be taken back. An agent reaches only the records this app
itself wrote: they carry its own sync identifier, and HealthKit will not let one app delete
another's, so nothing an agent sends can touch the watch's sleep or another app's readings. What a
change pushes out of Health is read in the same breath — the last moment anything can read it — and
kept on the phone, which is what lets the owner put it back: an addition is removed again, while a
change and a removal write the displaced record back where it was. A delete of something that is
not there is notFound and bothers nobody.
Two words in the outcome vocabulary are history. awaitingApproval and declined come from a phone
that held a change back until its owner had answered; nothing does that now, and they survive only
so that outcomes already stored go on reading correctly.
The phone applies edits when it is opened or wakes to send, which is minutes to hours and never at once. Write access is asked for the first time the app is opened after the update; until it has been, edits wait rather than fail.
Everything an agent changes is written down on the phone, because nowhere else keeps it: the service is told counts and codes on purpose, and Health keeps a record without keeping who asked for it. An app that writes into Health silently is an app nobody should trust with Health, so the everyday screen shows a note at the top whenever a run has landed that nobody has looked at — how many records changed, and one way to take the whole run back out. While anything is waiting for a decision the same strip asks for it instead, and leads to one screen that shows everything waiting and carries two actions: allow them all, or turn them all down. There is no per-record answer, because the decision a person actually makes is about the run in front of them, and a screen that asked about each item separately would turn one answer into a chore nobody finishes — which ends with an agent waiting forever on a question that was read and left. A row above the keys opens the list of everything an agent has ever done. Each line says the metric in the person's own word, the value and when it arrived; one that failed says why in a sentence rather than a code. A record still standing in Health can be taken back out from its own page or by swiping its row: the sample leaves Health, the day it was on is marked, and it goes to the archive again, so the archive stops showing it too. A record the agent removed offers no undo, because the old value was never kept — the agent hands over an id and the record is gone before anything can read it. The phone can also say a run has landed while the app is closed, in a notification that counts records and names neither a metric nor a value. The walkthrough offers that permission and takes "not now" for an answer without asking the system anything, so a phone that walked past it is asked once more — the first time an agent actually writes something.
deno task efferent edits # every edit and what became of it, by name
deno task efferent write --items meal.json # put entries into Health through the phone
Writing is the MCP server's: phone_data_write takes the items and phone_data_edits says what
became of each, and the overview names what may be written and in which unit. The command line
writes through the same tool, and the Python reference returned by setup_guide writes too —
--write <items.json> and --edits — so the public connection path needs nothing from this
repository in either direction.
Nobody but you, and the service holding the data least of all.
The phone creates the reading key pair and the archive before an agent is involved. It keeps the public half for sealing days and keeps the private half in its Keychain so the owner can hand it to an agent. The bucket is named by the hash of the public key, which is why there are no accounts here: knowing where to write follows from knowing which key seals the archive.
The handoff gives the agent a remote MCP URL with the bucket id in its path and the reading private
key as a separate field. Its instruction says to call setup_guide first. The key is stored on the
agent's machine and never sent to the remote MCP endpoint, Cloudflare, an HTTP header or a tool
argument. Cloudflare can return setup text, list and return sealed days, but cannot open one. The
agent fetches ciphertext and decrypts and analyses it locally.
The bucket id is an address, and it is kept nowhere safe: it sits in the MCP URL, so it is in every
client configuration the owner adds it to — some of them hosted by somebody else — and in the
service's request logs. So it opens nothing on its own. The phone makes a read key from the
reading key — an Ed25519 key whose seed is HKDF-SHA256 over the raw reading private key, with an
empty salt and the info efferent/v1 read — and registers its public half with the service, signed
by the writer key. From then on every read of a day, a range, a listing or an outcome must be signed
with it, over the bucket, the path and query exactly as sent, and a moment within five minutes of
the service's clock; a signature for one range opens no other. Every holder of the reading key makes
the same read key, so the handoff carries nothing new and the reading key itself never travels.
What stays open is how much there is: whether the archive exists, how many days and bytes, its first
and last day. An archive joined from a reader before the phone made its own keys has no reading
private key on the phone, so it never registers a read key and stays readable by its id, as before.
Encryption says nothing about authorship, so the phone also makes a signing key on first launch. It claims the empty archive with that key and signs every later upload; afterwards only that writer is accepted — otherwise a stranger who learned a bucket name could fill it with rubbish. That check carries more weight now than it did: writing over a day is the ordinary operation, so without it a stranger could overwrite history rather than merely add to it. The signing key cannot decrypt, and the reading key cannot write: handing an agent the ability to read must not hand it the ability to forge.
A day on the wire is the raw-deflate-compressed columnar object above, inside an RFC 9180
base-mode HPKE envelope —
[version 2][32-byte encapsulated key][ciphertext and tag] — with the bucket name and the date bound
into the authenticated data. The suite is DHKEM(X25519, HKDF-SHA256), HKDF-SHA256 and
ChaCha20-Poly1305. CryptoKit seals on the phone, and the Node client and the Python reader open,
each implementing HPKE itself — the Node client over Node's own modules, the Python reader with
PyCA cryptography as its only dependency — because a reader an agent is told to install must
name nothing nobody has audited. Both retain the custom X25519/HKDF/AES-GCM version 1 decoder
while stored days are replaced; new writes never use it.
Days travel a month at a time, because the request is what costs rather than what is in it: a day is a few kilobytes and the first export is thousands of them, so a request each would be a phone spending its whole waking life on round trips. What goes up is a plain frame — a date, a length, a sealed blob, repeated — and the signature covers the whole of it along with the dates it names, so the service has to prove its own reading of the frame before it can store anything. Each day inside is still sealed to its own date, so a batch binds nothing and ends at the door: the archive never learns that days arrived together. They come back the same way: a range is one request in the same frame, signed by the read key rather than the writer.
There is no server-side invitation or pairing session. The phone creates the bucket and gives the
connection handoff directly to the agent. documents/connection.md
defines its fields and responsibilities. Existing build-7 destinations remain readable and keep
uploading, but fresh setup is phone-first and the scanner has been removed. The on-device ledger is
bound to the bucket whose writes its fingerprints describe: activating another phone-owned archive
invalidates those claims and queues every known day again, while a legacy destination is adopted
without a reset.
src/Core describes these bytes in Swift and the Node client and the Python reader describe them
again, so deno task interop exists to prove the three still agree: a Swift test packs, seals and
signs a real request of two days, and both readers unpack it, open each day and check the
signature. Two days
rather than one, because a batch of one would never cross the boundary where a framing disagreement
would live. The read key is checked the same way: both sides make it from one reading key, and the
reader verifies a read the phone signed. With --post <url> it also puts that request through a running service and reads both
days back out separately.
The service itself is a Cloudflare Worker over an R2 bucket, answering at api.efferentapp.com. It
is operated separately from this repository and is not built here. A second, separate copy of it
answers at api-dev.efferentapp.com over its own bucket, for a development build of the app, so
nothing a development build does lands beside a real archive. The service is open to the internet by design — there are no
accounts, and your bucket is safe because the first writer keeps it. Creating one, though, is what
costs storage for years, so a bucket is created only for a caller Apple vouches for: the claim
carries an App Attest attestation over the very bytes the writer key signs, and the service checks
Apple's certificate chain itself. An upload no longer brings a bucket into existence. That makes a
new archive cost a real iPhone running this app rather than a loop in a shell.
The bill is defended separately, because a caller Apple vouches for can still be careless: a day may weigh a mebibyte and a request five, a day must be a date between 1900 and tomorrow, one bucket may be handed 512 MB and the service 100 GB in total, and claims and uploads are counted per caller. For scale, an eleven-year archive of one person is 37 MB.
Both of those tallies count bytes ever handed over and never come down, and the service says nothing
before the end of one, where every upload is answered 507. Removing an archive when its owner asks
is done by a maintenance tool on the operator's own machine: the service has no delete route, on
purpose.
deno task check
check — the secret scan, lint and types on the scripts, the reading side's format, lint and
tests (the Node client and the Python reference), then a simulator build.test — unit tests on any available iPhone simulator.dist — unsigned App Store archive at build/Efferent.xcarchive.fmt — format task scripts, and Swift if swiftformat is installed.secrets — scan the working tree and the whole history for committed keys (brew install gitleaks). Part of check, and the only thing GitHub runs on a push.generate — regenerate the Xcode project from Project.swift.icons — re-render the app icons from documents/icon.svg.screenshots <directory> — the six store screenshots at 1290 × 2796, drawn offscreen by the app itself
(--snapshot <directory>) from made-up figures and a key invented on the spot. No phone, no Health, no network.interop — check that Swift, the Node client and the Python reference agree on request bytes, HPKE, the day's own layout, the
phone handoff key and the read key, and that the phone opens and verifies an edit the Node client
sealed and signed.
It runs the RFC 9180 self-test first, on the exact source setup_guide returns.reader:setup — make reader/.venv from Python 3.13 and install the one run-time dependency and
the formatter. The Python reference and the checks that use it need it; EFFERENT_PYTHON points
at another interpreter instead.test:reader — the reading side's format check, lint and tests, without the Swift half: the Node
client's own self-test and suite on Node 22 or newer (EFFERENT_NODE points at another node),
then the Python reference's.efferent — the Node client: connect --handoff <file>, the tool commands overview, daily,
statistics, sleep, workouts, samples, write and edits, each answering in the shape of
the matching MCP tool, tools and call, and sync, status, query, ask and read over
raw events. connect --handoff - reads the handoff from standard input without putting the key
in a process argument. An option the command does not read is refused.EFFERENT_HOME is not optional in practice. Unset, the reading side falls back to .efferent
relative to the working directory, so a stale profile left in a checkout answers as if it were the
live one — with a retired bucket's days, a plausible day count and no error at all. Name the profile
directory on every run, and keep retired profiles under a name the fallback cannot reach.
Trying the phone-first path without a phone needs a development copy of the service running
somewhere you can reach. Create an archive in the app, share its four-field handoff into a private file, then set a fresh
EFFERENT_HOME and run deno task efferent connect --handoff <file>. Delete the temporary file
after import. deno task phone send still stands in for a phone during protocol development and writes as many days
in one request as are named.
One thing to watch when writing into a service that keeps its data: the first writer owns a bucket
for good, so a test upload claims it and the phone is refused afterwards. Release the claim by
deleting <bucket>/key from R2.
Signing, packaging and upload all happen outside this repository. Nothing here touches a certificate, and the archive path above is the whole of the agreement with whatever does.
src/Core/Sources/Health — the metric catalogues, the readers, the day builder, and the writer
that puts an agent's items into Health.src/Core/Sources/Wire — the event type, the day, the columnar body, the request frame, the edit.src/Core/Sources/Store — schema, day ledger, anchors, the versions behind written ids.src/Core/Sources/Upload — Keychain, background upload, reading the archive's listing, and the
applier that collects edits and reports what became of them.src/App/Sources — the setup walkthrough, the everyday screen, the design, the composition root.src/Tests/Sources — unit tests.documents/server-costs.md — the measured marginal storage and operation cost per user.documents/requirements.md — what the app must do, subsystem by subsystem, with an
identifier per requirement.documents/design.md — how it is arranged to meet them.reader/efferent.mjs — the reading client: the wire, the profile, the service, the analysis, the
nine tools, the local MCP server and the command line, in one file on Node's own modules.reader/tests/node/ — its tests, the fake archive they run against, and its half of
deno task interop.skill/efferent/SKILL.md — the agent skill that carries the client.reader/efferent_hpke.py — the source setup_guide hands an agent: HPKE, the day layouts and
enough of the archive to fetch and open one. Every other reading module builds on it.reader/efferent/archive.py — where days come from and where edits go: the keys, the service, the
mirror.reader/efferent/analysis.py — days turned into answers, and every correction that turning needs.reader/efferent/cli.py — the Python reference as a command line tool.reader/efferent/mcp.py — the Python reference as an MCP server.reader/efferent/interop.py — the reading half of deno task interop, and the fixture the Swift
half opens.phone — the Python reference's command line: keygen and send, which stand in for a phone
during protocol development.mcp — the same archive as an MCP server on stdio, for an agent to read and write.skill — assemble the agent skill in build/skill/efferent/ and run its client's self-test.