Database
TrickBook uses MongoDB Atlas for data storage.
Connection
Database: TrickList2
Host: MongoDB Atlas (cloud)
const { MongoClient } = require("mongodb");
const ATLAS_URI = process.env.ATLAS_URI;
// mongodb+srv://user:pass@cluster.mongodb.net/TrickList2
const client = new MongoClient(ATLAS_URI);
await client.connect();
const db = client.db("TrickList2");
db.js opens one pooled MongoClient at boot and every route factory receives the shared db handle. Route files do not open their own connections.
Schema layer
The backend uses the raw driver, not Mongoose, so for a long time a collection's shape was whatever each insertOne wrote. Since September 2026 every collection the code touches has a declared $jsonSchema in schemas/collections/*.js (49 collections, grouped by domain: identity, tricks, spots, feed, media, social, community).
How it works:
schemas/index.jsloads the files into a registry.test/schema-registry.test.jsfails CI when anycollection('name')call in the code has no schema, so the registry cannot drift silently.- On every boot
schemas/apply.jspushes each schema to Atlas as a collection validator withcollMod. It never creates a collection, and a failingcollModis logged, not fatal. SCHEMA_VALIDATION_ACTIONiswarnby default (violations go to the server log only),errorrejects non-conforming writes,offskips the apply.SCHEMA_VALIDATION_LEVELismoderate(inserts and updates to already-valid documents) orstrict.npm run schema:reportsamples the newest documents in each collection and validates them app-side withschemas/validate.js. This is the feedback loop on the free Atlas tier, where the server's warn log cannot be read. Promote a collection toerroronly after the report is clean for it.
Every schema is open (additionalProperties is never false) because production holds fields the current code no longer writes. Each entry carries writers (the files that insert or $set it) and notes that record where writers disagree.
User id shapes
req.user.userId comes out of the JWT as a 24-character hex string. Some writers store it as that string, some convert it with new ObjectId(), and rows from the original app hold a DBRef (tricklists.user). The schemas list every shape that exists per field rather than the shape we wish existed. utils/ids.js (toObjectId, idEquals, anyIdShape) reads and queries across them. New code stores ObjectId; converging the existing data is a separate migration.
The field lists that follow predate the schema layer and are illustrative. The files under schemas/collections/ are the source of truth for field names and types.
Collections
users
User accounts and subscription information.
{
_id: ObjectId,
name: String, // Display name
email: String, // Unique, used for login
password: String, // bcrypt hashed (null for SSO users)
imageUri: String, // S3 URL for profile picture
role: String, // "admin" or null
isGoogleSSO: Boolean, // true if registered via Google
appleUserId: String, // Apple Sign-In identifier
sports: [String], // Array of sport types
riderProfile: Object, // User's rider info
// Social features
network: Object, // User connections
homies: [ObjectId], // Array of friend user IDs
homieRequests: {
sent: [ObjectId], // Outgoing requests
received: [ObjectId] // Incoming requests
},
subscription: {
plan: String, // "free" or "premium"
status: String, // "active", "canceled", "past_due"
stripeCustomerId: String,
stripeSubscriptionId: String,
currentPeriodEnd: Date,
lastPaymentDate: Date,
adminOverride: Boolean // For testing
},
createdAt: Date,
updatedAt: Date
}
Indexes:
email(unique)
tricklists
User's personal trick lists.
{
_id: ObjectId,
name: String, // List name
user: ObjectId, // Reference to users._id
completed: Number, // Count of checked tricks
tricks: [
{
_id: ObjectId,
name: String,
checked: Boolean
}
]
}
Relationships:
user→users._id
tricks
Individual tricks (legacy collection, embedded in tricklists).
{
_id: ObjectId,
list_id: String, // Reference to tricklists._id
name: String,
checked: Boolean
}
trickipedia
Global trick encyclopedia (admin-managed).
{
_id: ObjectId,
name: String, // Trick name
category: String, // e.g., "Flip Tricks", "Grinds"
difficulty: String, // e.g., "Beginner", "Intermediate"
description: String, // Full description
steps: [String], // Step-by-step instructions
images: [String], // S3 URLs
videoUrl: String, // YouTube/external video
source: String, // Attribution
url: String, // URL slug for SEO
createdAt: Date,
updatedAt: Date
}
Indexes:
url(unique)categoryname(text index for search)
spotlists
User's spot list collections.
{
_id: ObjectId,
name: String,
description: String,
userId: String, // User's ObjectId as string
spotIds: [ObjectId], // References to spots
createdAt: Date,
updatedAt: Date
}
Relationships:
userId→users._idspotIds[]→spots._id
spots
Skate spot locations.
{
_id: ObjectId,
name: String,
latitude: Number,
longitude: Number,
images: [String], // Array of image URLs
description: String,
rating: Number, // 0-5
tags: String, // Comma-separated
city: String,
state: String,
sportTypes: [String], // skateboarding, bmx, etc.
category: String, // park, street, indoor, diy, other
userId: ObjectId, // Creator reference
createdAt: Date,
updatedAt: Date
}
Indexes:
- Geospatial index on
{latitude, longitude}(recommended)
feed_posts
Social feed posts with media content.
{
_id: ObjectId,
userId: ObjectId, // Creator reference
caption: String,
description: String,
mediaType: String, // "video" or "image"
mediaUrl: String, // CDN URL
thumbnailUrl: String, // Video thumbnail
tricks: [String], // Associated trick names
stats: {
loveCount: Number,
respectCount: Number,
commentCount: Number,
shareCount: Number,
viewCount: Number
},
engagement: {
completionRate: Number
},
createdAt: Date,
updatedAt: Date
}
reactions
Love/respect reactions on feed posts.
{
_id: ObjectId,
userId: ObjectId,
postId: ObjectId,
type: String, // "love" or "respect"
createdAt: Date
}
feed_comments
Comments on feed posts.
{
_id: ObjectId,
postId: ObjectId,
userId: ObjectId,
content: String,
createdAt: Date
}
saved_posts
User bookmarked posts.
{
_id: ObjectId,
userId: ObjectId,
postId: ObjectId,
createdAt: Date
}
conversations
Direct message conversations.
{
_id: ObjectId,
participants: [ObjectId], // User IDs
lastMessage: Object, // Preview of last message
createdAt: Date,
updatedAt: Date
}
dm_messages
Individual direct messages.
{
_id: ObjectId,
conversationId: ObjectId,
senderId: ObjectId,
content: String,
read: Boolean,
createdAt: Date
}
blog
Website blog posts.
{
_id: ObjectId,
title: String,
author: String,
date: Date,
content: String, // HTML or Markdown
url: String, // URL slug
images: [String] // S3 URLs
}
Indexes:
url(unique)
categories
Trick categories for the app.
{
_id: ObjectId,
name: String, // e.g., "Flip Tricks"
icon: String, // Icon name for MaterialCommunityIcons
backgroundColor: String, // Hex color
color: String // Text color
}
expoPushTokens
Push notification tokens.
{
_id: ObjectId,
token: String, // ExponentPushToken[xxx]
userId: ObjectId
}
messages
Contact form submissions.
{
_id: ObjectId,
name: String,
email: String,
message: String,
createdAt: Date
}
Data Relationships
users
│
├──< tricklists (user field)
│ │
│ └──< tricks (embedded or list_id)
│
├──< spotlists (userId field)
│ │
│ └──< spots (spotIds array)
│
├──< feed_posts (userId field)
│ │
│ ├──< reactions (postId field)
│ ├──< feed_comments (postId field)
│ └──< saved_posts (postId field)
│
├──< conversations (participants array)
│ │
│ └──< dm_messages (conversationId field)
│
├──< homies (users.homies array → users._id)
│
└──< expoPushTokens (userId field)
trickipedia (standalone, admin-managed)
blog (standalone, admin-managed)
categories (standalone, admin-managed)
Query Examples
Get User's Trick Lists with Tricks
const tricklists = await db.collection("tricklists")
.find({ user: new ObjectId(userId) })
.toArray();
Search Trickipedia
const tricks = await db.collection("trickipedia")
.find({
$or: [
{ name: { $regex: searchTerm, $options: "i" } },
{ description: { $regex: searchTerm, $options: "i" } }
],
category: categoryFilter,
difficulty: difficultyFilter
})
.toArray();
Get Spots in List
const spotlist = await db.collection("spotlists")
.findOne({ _id: new ObjectId(listId) });
const spots = await db.collection("spots")
.find({ _id: { $in: spotlist.spotIds } })
.toArray();
Update Subscription
await db.collection("users").updateOne(
{ _id: new ObjectId(userId) },
{
$set: {
"subscription.plan": "premium",
"subscription.status": "active",
"subscription.stripeSubscriptionId": subscriptionId,
"subscription.currentPeriodEnd": new Date(periodEnd * 1000)
}
}
);
Recommended Indexes
// users
db.users.createIndex({ email: 1 }, { unique: true });
// tricklists
db.tricklists.createIndex({ user: 1 });
// trickipedia
db.trickipedia.createIndex({ url: 1 }, { unique: true });
db.trickipedia.createIndex({ category: 1 });
db.trickipedia.createIndex({ name: "text", description: "text" });
// spotlists
db.spotlists.createIndex({ userId: 1 });
// spots (for geospatial queries)
db.spots.createIndex({ latitude: 1, longitude: 1 });
// blog
db.blog.createIndex({ url: 1 }, { unique: true });
Backup Strategy
MongoDB Atlas provides:
- Continuous backups
- Point-in-time recovery
- Snapshot backups
Access via Atlas dashboard → Backup → Snapshots.