Care Codes: Sharing Pet Care Without an Account — PupPal
2026-08-09
Care Codes: Sharing Pet Care Without an Account — PupPal
Every pet owner knows the moment. You are leaving for a long weekend, the sitter arrives, and you spend twenty minutes walking them through the routine: the food, the water, the treats, the anxious cat hiding under the sofa, the dog who will absolutely bark at the mailman. Then you leave, and for the next three days you wonder whether any of it is actually happening. PupPal's answer to that moment is the care code — a short sharing credential, inspired by RustDesk's session model, that turns any friend or family member into a monitored caregiver without asking them to create a single account. The owner snaps a photo, the system generates a code, and the caregiver opens a link, types a PIN, and starts checking in on the pet. No registration, no app install, no profile, no password to forget.
This post is a deep dive into how that mechanism works end to end: how a care code is generated, how the PIN is stored and verified, what happens inside the Cloudflare Worker when a caregiver authenticates, how the security model resists guessing and brute force, and how care sessions change the monitoring that runs on every photo. It is the third leg of the product story told in the earlier posts — the pipeline behind every photo check-in and the anomaly engine that watches each result — but it solves a different problem. Those posts were about perception: understanding what is in a photo. This one is about trust: letting someone else act on your behalf without giving them the keys to everything.
Why Pet Care Is a Shared Problem
Before looking at the mechanism, it is worth being precise about the problem. PupPal is built around a simple loop: photo equals check-in, sharing equals care. The owner's side of that loop is well covered — snap a picture, let Hermes Agent analyze it, get a status update. The second half, the sharing half, is where most pet products quietly fall apart, because they try to solve sharing with accounts.
The account tax. If a caregiver needs an account, the product inherits a whole onboarding funnel that has nothing to do with pet care: email, password, verification, consent, forgotten-password recovery. The neighbor who is happy to feed your cat for three days is not going to install an app and create an account to do it. The friction is not a small tax on the experience; it is the reason the experience never happens at all.
The permission problem. Accounts also force a binary choice about authorization. Either the caregiver gets a full account with access to everything — your pet's photo history, your habits, your settings, potentially your other pets — or the product builds a fragile invitation-and-roles system that itself becomes a second product. The caregiver needs a very small, very specific slice of capability: see the pet's care instructions and upload check-in photos for a limited window of time.
The accountability gap. Finally, when a human is watching your pet in your absence, you want a record. The product should know who did what, when, and what the pet looked like at that moment — not for policing, but because that record is the difference between "I hope everything is fine" and "I can see the pet was eating and resting at these times, and the analysis flagged nothing unusual." Sharing, done right, is not a loss of information; it is a new source of it.
The design constraint that follows from all three problems is unusual: the sharing mechanism must be more secure than a shared password, while being easier than typing a password. That is exactly the niche the care code fills. It is a short credential with a bounded lifetime, scoped to one pet and one session, backed by server-side rate limiting and signed tokens, and it requires nothing from the caregiver except the ability to open a link and type four digits.
The RustDesk Pattern: Credentials Over Accounts
The phrase "RustDesk pattern" appears in the PupPal documentation for a reason. RustDesk, the open-source remote-desktop tool, solves a similar problem in a different domain: two people who have never met need to establish a temporary, secure connection, and neither wants to create an account to do it. Its mechanism is a session ID plus a password — the host displays a short numeric identifier and a temporary password, the client enters both, and a connection is established without either party having an identity on a central server.
PupPal borrows that shape and adapts it to pet care. The analogies map cleanly:
- Session ID → care code. RustDesk's host ID becomes a code in
the form
PUPPY-XXXX— a short, human-readable, typeable string that identifies one care session. - Temporary password → PIN. The host's one-time password becomes a four-digit numeric PIN that the owner shares out-of-band, by message or in person.
- Connection lifetime → session window. RustDesk's session is
live while both ends are connected; a care session is live between
an optional
start_timeandend_time, or indefinitely until the owner ends it.
What makes the pattern powerful in both products is what it refuses to do. There is no user model for the guest. There is no profile to create, no email to verify, no social graph to join. The credential itself is the identity: possession of the code and the PIN is the entire authorization story. That is a radical simplification on the client side, and it moves all the hard security work to the server, where it belongs.
The trade-off is equally clear, and PupPal embraces it: because the credential is the identity, the credential must be short-lived, must be rate-limited, and must not be derivable from anything the caregiver can observe. The rest of this post is essentially the details of how those three properties are engineered — in the generation logic, in the verification endpoint, and in the token that gets issued after verification succeeds.
Birth of a Care Code: Inside care-create
A care session begins when the owner taps "start care" in the app.
The app sends a request to the Worker's puppal-care-create
endpoint with the pet's ID and, optionally, start and end times.
What happens next is a small choreography of generation, storage,
and webhook dispatch, and every step has a reason.
Generating the code. The Worker's generateCareCode() function
builds the string PUPPY- followed by four random characters drawn
from a carefully chosen alphabet:
ABCDEFGHJKLMNPQRSTUVWXYZ23456789. Notice what is missing: the
letters I and O, and the digits 0 and 1. Those characters are
excluded because they are visually confusable — an uppercase I
looks like a lowercase l and a 1, and O looks like 0. When a
caregiver is typing a code from a screenshot or a message on a phone
keyboard, every excluded character is a class of typo that simply
cannot happen. The code space is still large: 32 characters, four
positions, roughly a million combinations, which matters because the
code is only half of the credential.
Generating the PIN. The PIN comes from generatePIN(), which
produces a four-digit number between 1000 and 9999. It is deliberately
numeric — caregivers will type it on a phone keypad, and the H5 page
renders the input with inputmode="numeric" so the caregiver gets a
numeric keyboard. It is deliberately four digits: long enough to
feel like a credential, short enough to read aloud over a phone call
("three three six nine").
Hashing the PIN before it is stored. The Worker never stores the
PIN. It computes pin_hash = sha256Hex(pin, secret) — a salted
SHA-256 using the CARE_CODE_SECRET environment variable — and
stores only that hash in the session record. A database leak, a KV
misconfiguration, or a log that accidentally captures the session
record does not reveal the PIN, and because the hash is salted with
a server-side secret, even a rainbow-table attack against the small
four-digit space is not viable: an attacker would need the secret to
even compute candidate hashes. The plaintext PIN exists in exactly
two places: the Worker's response to the owner's create request, and
the owner's local session history in the app, where the UI shows it
once at creation time.
Storing the session. The session object is written to Cloudflare
KV under the key care:{care_code} with a TTL of 30 days. The
record contains dog_id, care_session_id (generated as
care_<timestamp>), pin_hash, start_time, end_time, status
(active or ended), created_at, and a handbook field that
starts as null. The 30-day TTL is the last line of defense: even if
everything else fails, the record physically expires. KV is the right
store here — care sessions are read rarely and written occasionally,
they have a natural expiry, and they must be readable from any
Worker instance, which rules out in-memory state.
Firing the care-create webhook. With the session persisted, the
Worker calls Hermes Agent's puppal-care-create webhook. This is
where the care-handbook skill runs: the agent generates a care
handbook for the pet — feeding instructions, habits, the anomaly
thresholds in force, anything a stranger would need to step in
immediately. The handbook returned by Hermes is then snapshotted
back into the KV session record. That snapshot matters more than it
looks: if Hermes is slow or unavailable when the caregiver later
verifies, the handbook still exists, because the verify endpoint
falls back to the snapshot when the live agent call returns nothing.
The session is usable even when the AI layer is having a bad day —
a deliberate resilience decision.
The response to the owner contains everything they need: the care
code, the PIN, the session ID, and the share link, which is built by
the app as {baseUrl}/c/{care_code} — a direct route into the
caregiver's web page. The owner copies the link, sends it to a
friend, and tells them the PIN by voice, message, or in person. The
caregiver is now one click and four digits away from caring for the
pet.
The Caregiver's Side: A Link, a PIN, No Account
The caregiver's entire experience lives on a single static web page
served by the Worker at /c/{care_code}. The page is a deliberate
engineering artifact: a single-file, inline-HTML page with no build
step, no external dependencies, no CDN references, and no fonts
loaded from anywhere. If a CDN is down or the network is flaky, the
page still renders, because there is nothing to fetch. The design
comment in the source is explicit about the rationale: a caregiver
on a poor connection, in a strange house, with a nervous pet should
never see a white screen because a third-party stylesheet failed to
load.
The three-card flow. The page walks the caregiver through three cards. First, the PIN card: the care code is already in the URL, so the only thing asked for is the four-digit PIN. Second, the handbook card: after verification, the page renders the AI-generated care handbook with a minimal Markdown renderer that handles headings, bold, lists, and paragraphs — the entire surface of a typical handbook. Third, the upload card: the caregiver can take or choose a photo, add an optional note (up to 500 characters, e.g. "lunch fed"), and upload a check-in.
Every error state is driven by the API, not by client-side guesses.
A wrong PIN produces a server-side 403 and an inline error message.
A session that has ended produces the "care session ended or link
invalid" card. Rate-limit responses produce their retry_after
value as a human message. The page never tries to be clever about
what the server means; it just displays what the server says.
Verification: three gates. When the caregiver submits the PIN,
the Worker's puppal-care-verify handler runs. Three gates must all
pass before anything is issued. First, the session must exist in KV
and have status === 'active'. Second, if the session has an
end_time, it must be in the future — a session whose window has
closed is treated exactly like a nonexistent one, returning the same
404, so an attacker cannot distinguish "this code was never valid"
from "this code expired." Third, the submitted PIN must hash to the
stored pin_hash, compared with a constant-time comparison
(timingSafeEqual) so that timing side channels cannot leak
information about how close a guess was.
The throttles that sit in front. Before any of that, the verify
endpoint applies rate limiting that lives in KV under
rl:care:{care_code}. The limits are aggressive and specific:
at most five verify attempts per code per minute, and a cumulative
failure counter that survives across windows — ten wrong PINs total,
and the code is locked for one hour with a 429 and a retry_after
header. This is PIN-brute-force protection designed for the actual
threat: a four-digit PIN has only 9000 possible values, so without
throttling it could be exhaustively guessed in minutes. With
throttling, an attacker gets five tries per minute and is locked out
after ten failures — exhaustive search becomes hours of waiting for
a lock that keeps resetting. The rate-limit record expires on its
own (TTL covers the longest possible lock), and a successful
verification deletes it entirely, so a legitimate caregiver is never
carrying an old failure count around.
The reward: a short-lived token. Verification success issues the
real credential: a care token. Its format is a three-part dotted
string — {care_code}.{exp_unix_seconds}.{signature} — where the
signature is HMAC-SHA256 over the payload
"{care_code}|{care_session_id}|{exp_unix_seconds}" using the
CARE_CODE_SECRET. The token's lifetime is the minimum of the
session's remaining time and seven days, so it can never outlive the
care window. This is the actual bearer credential for photo uploads:
the PIN is a one-time entry key, the token is the ongoing key.
The Security Model: What Is Signed, What Is Throttled
Care codes are a small enough surface that the security model can be stated completely, and it is worth doing so, because the whole design hangs on it.
The code and PIN are a two-factor pair. Neither half alone is sufficient, and the two halves travel differently. The code is in the share link, which means it can leak casually — screenshots, message logs, browser history. The PIN is transmitted out-of-band, by voice or in a separate message, and it is the half that is rate-limited and lockable. Even if a link leaks to a stranger, the stranger faces the throttled PIN gate; even if a PIN leaks, it is useless without the code. This is exactly the RustDesk model: the host publishes the ID freely, and the password is the real gate.
The PIN never leaves the server in usable form. Stored salted hash, constant-time comparison, plaintext present only in the creation response. There is no endpoint that returns the PIN, no reset flow that reveals it, no logging path that captures it. If a caregiver forgets the PIN, the owner — who has it in local session history — simply re-shares it; the server never needs to.
Tokens are signed, scoped, and short. The care token cannot be
forged without the CARE_CODE_SECRET (HMAC-SHA256), cannot be
replayed after its expiry (the exp field is checked on every
validation), and cannot be reused across sessions (the payload binds
it to a specific care_session_id). Validation requires three
independent facts to line up: a valid signature, an unexpired exp,
and a live session in KV. The token's job is narrow — uploading
photos for one pet during one care window — and its lifetime is
capped at seven days even for indefinite sessions.
Brute force is engineered out at two layers. At the PIN layer, the throttles described above. At the token layer, the 4-byte-plus HMAC signature makes forgery cryptographically infeasible regardless of effort. The remaining attack surface — an attacker who obtains a valid token — is bounded by the token's short lifetime and by the fact that the token is never persisted.
The browser keeps no secrets. The H5 page holds the care token
in a JavaScript variable only. It is explicitly never written to
localStorage or cookies, so a page reload loses it and a shared
device leaves no trace. The comment in the source is blunt about
this: a caregiver's phone is an untrusted environment, and the
session should die with the tab. The consequence is mild
inconvenience — a reload means re-entering the PIN — and a real
security property: there is nothing on the device to steal.
The server never learns more than it must. The Worker sees a care code, a PIN hash, a session ID, and photos. It does not see a social graph, does not maintain caregiver identities, and has no notion of "which friend is caring for the pet" beyond the credential they hold. This is the privacy-by-design posture described in the product overview: the smallest possible surface, by construction.
Care-Mode Monitoring: Stricter Eyes While Someone Else Watches
A care session does not just open a door for the caregiver — it changes how every photo that comes through that door is judged. This is the feature that makes care codes more than a sharing gimmick: the system knows a stranger is in charge, and it adjusts its vigilance accordingly.
In normal operation, the C7 anomaly engine treats a photo as
escalating when its anomaly_score crosses 0.7, and even that
crossing is handled as a graded event. During a care session, the
photos uploaded by the caregiver run through the care-monitor
skill, and every anomaly threshold is lowered by 40%. In concrete
terms, the care-mode cutoff for flagging a photo to the owner is an
anomaly_score of 0.42 instead of 0.7. The threshold constant lives
in the app's care model as careAnomalyScoreThreshold = 0.42, and
the logic is explicit: a care photo needs owner attention when the
analysis marks needs_owner_attention as true or when the score
exceeds 0.42.
Why the tighter threshold? Because the context is different. The owner knows their pet's normal; a caregiver does not. A slightly unusual posture that the owner would recognize as a quirk of their dog's sleeping position is, for a caregiver, information the owner should see immediately. The cost of a false alarm during care is a notification; the cost of a missed signal is a pet whose decline was observed by a well-meaning stranger who had no baseline to compare against. Lowering the threshold shifts the system's bias from avoiding noise to avoiding silence, which is the correct trade for the care window.
There is a second, subtler reason the threshold tightens: the photo-analyze pipeline that runs on the owner's photos has the pet's history and baseline to reason with, while a caregiver's photo is often the first data point in a new context — the pet in a strange house, at a different time of day, maybe anxious. The care-monitor skill compensates for missing context by being easier to trigger. This connects directly to the escalation semantics described in the C7 anomaly detection deep dive: anomaly flags during care are pushed to the owner immediately, so a weekend away produces a stream of lightweight, structured reassurance — "photo at 18:04, score 0.38, nothing to worry about" — and an instant alert the moment something crosses 0.42.
The app's care page mirrors this logic on the display side: a care
photo whose analysis is JSON with needs_owner_attention true or a
score above 0.42 gets a warning marker, while non-JSON text summaries
are never treated as anomalies. The rule is deliberately strict about
what counts as evidence, so the owner is never pinged about a
free-text guess.
Ending a Session, and What the Pattern Teaches
Every care session ends, and the ending is as carefully handled as
the beginning. There are three paths. The owner can end the session
explicitly from the app, which flips the session's status to
ended and fires the puppal-care-end webhook with reason: 'manual'. If the session had an end_time, a sweep function in the
Worker (expireCareSessions) finds sessions whose window has passed,
marks them ended, and fires the same webhook with reason: 'expired'. And regardless of either, the KV record's 30-day TTL
guarantees the credential physically disappears.
The webhook matters because ending a session is an agent event, not just a database update: Hermes Agent records the outcome, so the pet's history and the owner's memory include what happened during the care window. From the owner's perspective, the session leaves behind a complete artifact — the handbook, the timestamps, and the photo stream with per-photo analysis — viewable in the app's care history. That artifact is the accountability record described at the start of this post: care, done through care codes, produces more information than the owner would have had watching alone.
Stepping back, the care code pattern is a lesson in product design that extends beyond pet care. When a feature needs to give someone else a temporary, narrow, revocable slice of your world, the instinct is to reach for accounts, roles, and invitations — the machinery of permanent identity. PupPal's care codes argue for the opposite: make the credential itself the identity, keep it short and human-readable, bind it to a lifetime, throttle it like a door lock, and sign everything the server issues. The result is a mechanism that is easier for the guest than a password, harder to break than most password systems, and invisible to maintain because it expires on its own. The neighbor feeds the cat; the owner sleeps; the photos pile up with their quiet anomaly scores; and at the end, the code dies and the trust — like the session — closes cleanly.
Care codes are the sharing half of "photo equals check-in, share equals care," and they are live in PupPal today, in the Worker's routes, the KV session records, the single-file H5 page, and the tightened care-mode thresholds. If you have not yet read how the pipeline behind a photo check-in works or how C7 anomaly detection guards every result, those two posts complete the picture. The next frontier — foster care with even stricter monitoring, and the adoption module on the roadmap — builds directly on this foundation: every future mode of sharing will need the same three properties the care code has, a short credential, a bounded lifetime, and a server that never stops watching.