A Fail-Closed Identity Boundary for an Alexa Skill — Without Breaking the Household Already Using It
A working single-household medication-logging skill needed to become safe for invited households. I designed and delivered a fail-closed identity boundary — Alexa must resolve exactly one household before it touches any data — behind a feature flag, so the family already relying on it in production was never at risk.
larai-w/medication-promise-app (issue #12, tasks #22–#26, PR #33)
Context
Medication Promise lets a family record medication timings by voice through Alexa or from a web app, running in a private-access production deployment for one household. The Alexa Lambda, however, still read and wrote through a fixed legacy partition (USER#default-user). Acceptable for one private household — but not a valid path to an invited-household beta, where a skill must never operate on the wrong family's data.
Constraints
- Live production, real care use. The existing household uses this daily; a regression is a real-world harm, not a bug ticket.
- Health-adjacent + tenant isolation. A wrong membership lookup would let Alexa act on another household's medication data — the worst failure mode.
- Public repository. No pool IDs, client secrets, tokens, or health data anywhere in the repo.
- Solo, part-time, AI-assisted. Governance had to live in issues, acceptance criteria, tests, and sanitised docs.
What I did — and deliberately didn't do
I scoped #12 to a verifiable identity boundary for one linked household, and explicitly out of scope: public self-service signup, multi-household switching inside Alexa, and role management. Ambiguity (a linked user mapping to more than one active household) fails closed rather than guessing.
Architecture & security decisions
The data path moved from "construct the partition key from a fixed USER_ID" to resolve identity, then derive the partition:
- Read the linked-account access token from the Alexa request context.
- Verify it against the identity provider (Amazon Cognito user pool) — signature via the pool JWKS, issuer,
token_use=access, and client binding — usingaws-jwt-verify. The Lambda never trusts an unverified token payload. - Extract the stable provider subject (
sub), look up membership in a single-table DynamoDB model (USER#<sub>→MEMBERSHIP#<householdId>), requiring exactly one active membership. - Derive
HOUSEHOLD#<householdId>and read/write only within that partition.
Key decisions: one shared identity provider for Web and Alexa (Cognito), so both surfaces resolve the same sub; fail-closed everywhere — missing, invalid, expired, ambiguous, or disabled identities are rejected before any DynamoDB access, each mapped to a supportive, non-leaking spoken message, with no raw tokens logged; and injectable seams (token verifier and membership lookup are parameters) so the boundary is unit-testable without a real user pool.
Delivery & governance (the safe cutover)
The most important product decision was not flipping production. The handler runs in legacy or household mode via an ALEXA_HOUSEHOLD_MODE environment flag, defaulting to legacy. Production keeps the working single-household path until the linked flow is verified end to end on a real device; only then does the flag flip — and rollback is flipping it back. The migration is identity state, not record state: records already live in a household partition, so there's no risky backfill. Work was framed as a user story with acceptance criteria, split into tasks #22–#26, and shipped via reviewed PR #33 with CI green.
Outcomes
- Alexa has a dedicated, fail-closed identity boundary that resolves one household before any data access; the failure paths are covered by automated tests.
- Tests grew to 45 (from ~40), including an assertion that in
householdmode the settings path never falls back to the legacy reader — proving the release boundary, not just happy-path intent. - The production household is provably unaffected: the legacy path stays default until an explicit, reversible flag flip.
Honest limitations
- The cutover itself and on-device verification require the owner's AWS/Alexa-console access and are not yet done — this case study is the boundary and its proof, not the production flip.
- Scope is one active household per linked identity; multi-household users fail closed by design until a selection UX exists.
- Membership provisioning is Web-side work outside this boundary.
Lessons
- Isolation is a release gate, and gates need tests that prove the boundary — asserting the legacy partition is never touched in the new mode, not just that the new mode works.
- Feature-flag the identity cutover. For a system real people use daily, the safe default + reversible flip is worth more than shipping a week sooner.
- Fail closed on ambiguity. "Resolve exactly one household or refuse" is a smaller, safer first release than guessing.
Evidence
Repo: github.com/larai-w/medication-promise-app — issue #12, tasks #22–#26; PR #33. docs/ALEXA_ACCOUNT_LINKING_CONFIG.md (sanitised config), design docs, and alexa/ (household.mjs, cognito.mjs, dynamodb.mjs, index.mjs, tests).
Shipping something real people already depend on?
Release discipline for health-adjacent, multi-tenant builds — reversible cutovers, boundary tests, no drama.
Book a Free Consultation