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_time and end_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 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.