Skip to main content

Data inventory

Status: Proposed 2026-09-24, check-ins and catalogue fields added 2026-10-24

This is the contract behind the privacy promise. Any pull request that adds a personal field must add a row here first. Rows are grouped by the collection that holds them; the full schema is in the data model.

Visibility values: self (the member only), friends, organizer (of the event in question), grove (members of that Grove), public, admin (moderation only, never exported), nobody (stored, never read back except by the system).

Account​

FieldPurposeVisibilityRetention
emailSign-in and magic linksnobody (used to send mail)Until deletion
passwordHash (argon2id)Password sign-in, optionalnobodyUntil deletion or password removal
handleThe only name other members seefriends, grove, organizer, public (only on public posts)Until deletion
avatarKeyOptional picturesame as handleUntil deletion or removal
homeAreaOne of eleven SoCal regions, picked from a listself, and as an aggregate count per GroveUntil deletion
interestsChosen tags, used by Ivy and for event suggestionsself, IvyUntil deletion
providers[]Apple or Google subject ids for SSOnobodyUntil deletion
discoverableWhether friends can find you by handle (default off)selfUntil deletion
publicPostsEnabledWhether you can post publicly (default off)selfUntil deletion
rolemember or adminnobodyUntil deletion

Not stored, ever: real name, phone number, birthdate, street address, device GPS, contacts, device identifiers beyond a push token.

Sessions and sign-in​

FieldPurposeVisibilityRetention
sessions.tokenHashSHA-256 of the session tokennobody30 days sliding, or on logout
sessions.clientios, android, or web, so you can revoke a deviceselfWith the session
magic_links.tokenHashSingle-use sign-in linknobody15 minutes

Social graph​

FieldPurposeVisibilityRetention
friendshipsWho is friends with whomthe two members involvedUntil either deletes the friendship or an account
friend_invites.codeInvite codes shared in personselfExpires, default 7 days
grove_membersMembership and organizer roleself, organizers of that Grove; counts are publicUntil leaving or deletion

Activity​

FieldPurposeVisibilityRetention
event_rsvpsGoing or interestedself, organizer of that event; counts are public30 days after the event ends
posts, comments, reactions, saved_postsThe feedper post: friends, grove, or public; comments follow the postUntil deletion
place_reviewsPseudonymous review text. The handle is included only when that review set showHandle and the account still existspublic text; handle only if showHandleUntil deletion, then the row stays with userId cleared and showHandle forced false
place_listsSaved places. isPublic is stored for future friend sharing and is not published yetselfUntil deletion
place_checkinsOne visit confirmation per member per place per UTC month. Public responses return aggregates onlysee the check-in fields belowThe row stays after account deletion, with userId cleared, so the count stays
saved_mediaThe watchlist: a saved list of titles, never a historyselfUntil deletion
media_reactions"This moved me" and "I took action" on a title; the counts on the title are public, the rows are notselfUntil deletion
action_logPrivate impact journalselfUntil deletion
messages (ciphertext)Direct messagesparticipants90 days by default
companion_conversationsIvy chatsself24 hours unless pinned
push_tokensDevice push addressnobody30 days after last use
notification_preferencesWhat to sendselfUntil deletion

Check-ins​

A check-in is separate from a review so a visit confirmation cannot carry a handle. The server assigns the UTC month (YYYY-MM). The client does not send it. One member can confirm a place once in that month. The place page and GET /api/places/:id/check-ins return checkInCount, latestCheckInMonth, and verification (verified or unverified). They do not return who went, a rating, or a list of visits. There is no leaderboard and no first-visitor flag.

FieldPurposeVisibilityRetention
place_checkins.userIdThe monthly limit, and finding the row on account deletionnobodyCleared on account deletion. The row stays
place_checkins.monthThe UTC month bucketnobodyWith the row
place_checkins.veganLevelThe level the member confirmednobodyWith the row
place_checkins.ratingOptional. Stored with the row and returned by no routenobodyWith the row
places.checkInCountHow many check-ins the place haspublicWith the place
places.latestCheckInMonthThe newest check-in monthpublicWith the place
places.lastAgreeingCheckInAtDrives verification. Not serializednobodyWith the place

verification is verified when lastAgreeingCheckInAt falls inside the last 180 days, and unverified otherwise. A Disputed state is described on the verification page and is not implemented.

Server-side records that mention a member​

RecordWhat it holdsRetention
Access logs (pino)Route, status, duration, a hash of the session id. Never email, token, coordinates, or bodies.14 days
reportsWho reported what and why, for moderationUntil resolved plus 90 days
Aggregate statsCounts only, no idsIndefinite

Ingested public data​

places, events, organizations, media_items, guides, and (proposed) creators rows written by the ingest endpoint carry no member data. Their provenance fields describe a source, not a person:

FieldWhat it holdsRetention
source, sourceId, sourceUrlWhich public dataset or organization site the row came from, and its id thereWith the row
lastSeenAtThe last ingest run that carried the rowWith the row
adminEditedField paths an admin changed, so a re-ingest does not overwrite themWith the row

Organizer names, attendee lists, and personal social accounts are dropped at the source and never stored. The one exception is Creators: public figures who publish vegan content in public, listed by name with their own public site and channel handles so a member can follow a link out. No follower or view counts, no photos, and nothing read from a social platform are stored, and any creator can have their entry retired on request. The automation that runs ingest holds one credential, the ingest key, which can write only through /api/ingest/* and cannot read any collection. OpenStreetMap, Nominatim, Wikidata, TMDB, and Open Library receive only the script's own requests, never anything about a member. Facebook and Instagram are never called.

Catalogue fields​

These fields describe a public work or a public place. They are not member data, and account deletion does not touch them.

FieldWhat it holdsRetention
media_items.kinddocumentary, film, series, talk, short, or bookWith the row
media_items.authorsAuthor names copied from the Open Library catalogue. Not members, and not linked to accountsWith the row
media_items.externalIds.isbnOne ISBN on a book, ISBN-13 when the catalogue has a valid oneWith the row
media_items.externalIds.openLibraryThe Open Library work idWith the row
media_items.yearCatalogue year. The floor is 1000 so a pre-1900 book can keep its yearWith the row
places.tags value market:farmersA grocery that is a farmers or produce market. Same place type as a store; the tag marks the weekly marketWith the row

What third parties receive​

PartyWhatWhy
AWS SESYour email address and the sign-in linkTo deliver magic links and account mail
Apple, GoogleNothing from us. When you use SSO they tell us a subject id and, if you allow, an email.SSO
Bunny StreamVideo files you upload, already stripped of metadataTranscoding and playback
OpenFreeMapMap tile requests from your device (no key, no cookies)Map rendering
AnthropicYour handle, your chosen interests, and the text of your Ivy conversationRunning the companion
OpenRouter, and Microsoft Azure as the upstream provider it routes toWhen knowledge search ships: the text of a search Ivy runs for you (not your handle, interests, or conversation), and the public text of Guides, media, creators, places, and public events. Routed only to a zero-retention endpoint, fallbacks off (ADR-0014).Turning text into embeddings for search by meaning

No party receives your email for advertising, your location, your friends, or your RSVPs.