MongoDB & Why
The database is where reality lives. If the rest of the platform crashed and the database survived, Dashify could be rebuilt from scratch and lose nothing. That is why this page exists at all, it is worth a slow, careful look.
What MongoDB is
MongoDB is a document database. Instead of storing data in flat tables with rows and columns (the way a traditional database like Postgres or MySQL does), MongoDB stores nested objects called documents. A document is conceptually like a JSON object. A collection is a folder of documents.
A user document in Dashify might look something like this in plain English:
Inside the
userscollection there is a document for Alice. It records her name, her email, when she was created, the organisation she belongs to, the role she has in that organisation, an array of recent login timestamps, and a nested object describing her notification preferences. All of that lives inside one document.
Compare that to a traditional database, where the same information might be split across five tables (users, roles, login_history, notification_prefs, organisations) joined by foreign keys. The document model is closer to how the application thinks about the data.
Why we chose it
A few reasons mattered when picking MongoDB for Dashify.
The data is heavily nested. A work item has a list of labels, a list of watchers, a list of comments, a check in history, a list of components. In a relational database all of those become separate tables and every read becomes a multi table join. In MongoDB they are all one document.
The schema evolves rapidly. Dashify shipped twenty-five phased releases in less than a year. Each release added fields, sometimes whole new objects. Mongoose's schema with soft validation model lets us add a field without writing a migration; the platform handles the missing-field-on-old-documents case naturally.
It scales horizontally. When the platform grows, MongoDB's replica sets and sharding give a clear path to running across multiple machines without rearchitecting.
Mongoose is excellent. The Node.js library that wraps MongoDB is mature, deeply integrated with TypeScript, and supports schema level plugins (which is exactly what makes the automatic tenant isolation plugin possible).
The shape of the data
Every box on the diagram is a Mongoose model. Every model declares its own schema. The arrow ORGANISATION to USER means "the user document carries an organisationId referring back to its owning organisation." That organisationId is the tenantId that the multi tenancy layer cares about.
Some collections you will encounter:
- organisations, the tenants themselves. Name, package, settings.
- users, every person across every tenant. Always carries
tenantId. - packages, the subscription tiers (Free, Standard, Pro, Enterprise) and what they include.
- projects, workitems, sprints, worklogs, the project management module.
- chatchannels, chatmessages, the real time chat module.
- kbarticles, kbcategories, the knowledge base.
- announcements, the broadcast posts.
- invoices, customers, the billing module.
- auditlogs, the immutable record of every privileged action.
- apitokens, Personal Access Tokens, hashed.
- webauthncredentials, registered passkeys.
- ssoconfigs, per tenant SSO settings.
- aiconfigs, per tenant AI settings (model choice, embedded API key if hosted, etc.).
- notifications, in app notification rows.
- files, Cloudinary file metadata.
Soft deletes
Most collections do not actually delete documents when a user clicks "delete." Instead they mark the document with deletedAt: <timestamp> and a Mongoose plugin filters out deleted documents from every query unless you explicitly ask for them. This makes recovery from "I deleted that by accident" trivial and makes the audit trail cleaner.
A scheduled cron job (run by the worker) hard deletes anything that has been soft deleted for longer than the retention window.
Validation
Mongoose schemas declare what each field must look like, string vs number vs date, required vs optional, length limits, allowed values. Anything that fails validation is rejected at save time with a clear error.
For incoming HTTP requests, Zod validates the shape before the data reaches Mongoose. If the request payload is malformed (missing field, wrong type), the API rejects it with a 400 and never invokes business logic. By the time a piece of data reaches the database it has been through two validation passes.
Connections, pooling, and timeouts
Mongoose holds a connection pool to MongoDB, by default, around ten concurrent connections. That pool is shared across all the requests an API instance is currently handling, so it can serve hundreds of users at once without opening hundreds of connections.
If the database becomes slow, the pool fills up and new requests wait. Pino logs the wait. Prometheus measures it. Grafana shows it. If it gets bad enough, Sentry fires.
Backups and recovery
In production, the recommended setup is a managed MongoDB cluster (Atlas) with point in time recovery enabled, meaning we can restore the database to any second in the past 24 hours, not just the nightly snapshot.
In local development, the database is just a Docker volume. docker compose down -v wipes it; otherwise it survives restart.
What's next
- Tenant Isolation, the deep dive on the automatic
tenantIdfilter. - Indexes & Performance, how queries stay fast as data grows.
- Caching with Redis, what lives outside MongoDB and why.
Key takeaways
- Dashify uses MongoDB, a document database where each record is a nested object.
- The data model is rapid evolving and heavily nested, which fits document storage better than relational tables.
- Mongoose is the ORM. Its schema level plugins are how automatic tenant isolation is implemented.
- Documents are validated twice, once by Zod at the HTTP boundary, once by Mongoose at save time.
- Most collections use soft deletes so accidental deletions are recoverable.