Skip to main content

Data model

Status: Scaffolded 2026-09-24

Every collection is a Mongoose schema in vegan-grove-api/src/models, with timestamps: true, every reference an ObjectId, and every index declared in the schema. That last sentence is the whole lesson from The Trick Book, where 38 collections were shaped by whichever insertOne ran first and userId was an ObjectId, a string, or a DBRef depending on the file.

The field-level privacy view is the data inventory. This page is the structural view.

Relationships​

1 to many

sorted pair

joins

hosts

hosts

may locate

makes

writes

keeps

records

has

users

sessions

friendships

grove_members

groves

events

organizations

places

event_rsvps

posts

comments

reactions

place_reviews

place_lists

conversations

messages

action_log

companion_conversations

Collections by domain​

Identity​

CollectionKey fieldsIndexes
usersemail (lowercase), emailVerifiedAt, passwordHash?, handle, avatarKey?, homeArea, discoverable, publicPostsEnabled, role, providers[], interests[], deletedAt?unique email, unique handle, providers.provider + providers.subject
sessionsuserId, tokenHash, expiresAt, lastSeenAt, clientunique tokenHash, TTL expiresAt, userId
magic_linksemail, tokenHash, expiresAt, usedAt?unique tokenHash, TTL expiresAt

Social graph​

CollectionKey fieldsIndexes
friendshipsuserA, userB (sorted so the pair is canonical), status, requestedByunique userA + userB, userB
friend_invitesuserId, code, expiresAt, usesLeftunique code, TTL expiresAt
grovesname, slug, area, description, memberCountunique slug, area
grove_membersgroveId, userId, roleunique groveId + userId, userId
organizationsname, slug, type, description, website?, verified, adminUserIds[]unique slug

Places and events​

CollectionKey fieldsIndexes
placesname, slug, type, veganLevel, location (GeoJSON Point), address, city, area, tags[], photoKeys[], approvalStatus, submittedBy?, source, osmId?, ratingAvg, reviewCountunique slug, 2dsphere location, approvalStatus + type, sparse unique osmId
place_reviewsplaceId, userId, rating, content, visitedMonth, showHandle, statusplaceId + createdAt, userId
place_listsuserId, name, placeIds[], isPublicuserId
eventstitle, slug, type, startsAt, endsAt, location, placeId?, venueName, address, detailsAfterRsvp, hostType, hostId, description, coverKey?, visibility, rsvpCount, status, createdByunique slug, 2dsphere location, startsAt + status, hostType + hostId
event_rsvpseventId, userId, statusunique eventId + userId, userId

Feed​

CollectionKey fieldsIndexes
postsuserId, mediaType, imageKeys[], bunnyVideoId?, caption, visibility, groveId?, placeId?, eventId?, stats, statususerId + createdAt, visibility + createdAt, groveId + createdAt
commentspostId, userId, parentId?, content, statuspostId + createdAt
reactionspostId, userId, typeunique postId + userId
saved_postspostId, userIdunique postId + userId
reportstargetType, targetId, reportedBy, reason, statusstatus + createdAt

Messages​

CollectionKey fieldsIndexes
conversationsparticipantIds[] (sorted), lastMessageAt, unreadunique participantIds, participantIds + lastMessageAt
messagesconversationId, senderId, ciphertext, iv, keyId, type, expiresAtconversationId + createdAt, TTL expiresAt

Content and companion​

CollectionKey fieldsIndexes
media_itemstitle, slug, kind, year, synopsis, posterKey?, watchLinks[], trailerYoutubeId?, tags[], featured, statusunique slug, status + featured
guidestitle, slug, category, body, statusunique slug, category
action_loguserId, type, eventId?, hours?, note?, occurredAtuserId + occurredAt
companion_conversationsuserId, messages[], pinned, expiresAt?userId, TTL expiresAt

Notifications​

CollectionKey fieldsIndexes
push_tokensuserId, token, platform, deadAt?unique token, TTL deadAt
notification_preferencesuserId, eventReminders, friendRequests, messages, quietHours?unique userId
scheduled_notificationsuserId, kind, payload, scheduledFor, status, idempotencyKeyunique idempotencyKey, scheduledFor + status

Conventions​

  • Derived counters (memberCount, rsvpCount, ratingAvg, reviewCount, stats) are maintained by the service that owns the write and recomputed by a nightly worker. They are never trusted for permission checks.
  • TTL indexes carry the retention policy: sessions, magic links, invites, messages, unpinned companion conversations, dead push tokens.
  • Soft delete exists only on users (deletedAt) so that a deleted account's handle is released after a cooling period. Every other deletion is hard.
  • Geo queries are bounding-box $geoWithin on the 2dsphere index. There is no $near query anywhere, because $near needs a point, and the client never sends one.