+1 (478) 654-9062

Schemas in an Inherited Meteor App: SimpleSchema, Collection2, and What Comes After

MongoDB does not care what shape your documents are. Somewhere in a Meteor codebase, something else has to, and in the apps we inherit that something is almost always the same pair: aldeed:simple-schema attached to collections through aldeed:collection2, written between 2015 and 2018, and never revisited since.

It is a layer that works quietly for years and then becomes interesting all at once — during a Meteor 3 upgrade, when a required field turns out not to be required, or when someone asks what would have to be rebuilt to move this data somewhere else. This note is about reading that layer honestly before you change it.

What the schema layer is actually doing

In a typical Blaze-era app, four different things are all called "validation" and they are not the same thing:

  1. check() and audit-argument-checks on method and publication arguments. This is a security boundary: it decides whether a hostile client can send you an object where you expected a string. Covered in more depth in our security pass note, but worth naming here because teams often assume schemas do this job. They do not.
  2. SimpleSchema definitions describing document shape — types, optional, allowedValues, min/max, regEx, autoValue, defaultValue, custom validators.
  3. Collection2 attachment (Collection.attachSchema(...)), which runs those definitions on every insert and update that goes through the Meteor collection API, cleans the document, and throws on violation.
  4. Form-level validation — AutoForm in the Blaze apps, a React form library in the newer ones — which usually reads the same schema and produces the messages users see.

The important asymmetry: only number 3 is enforcement at the data layer, and it only applies to writes that go through the Meteor collection API on the server. A rawCollection() call bypasses it. A migration script using the driver directly bypasses it. Anything that ever wrote to that database from outside the app — an import job, a partner integration, a one-off fix in a Mongo shell at 2am in 2019 — bypassed it too.

So the first useful question is not "what do the schemas say" but "what does the data look like."

Audit the data before you trust the schema

We run this early in an assessment because the answer shapes everything downstream, and it takes an afternoon. For each large collection, sample the actual documents and compare against the declared schema:

  • Count documents missing each field the schema calls required.
  • Count documents where the BSON type differs from the declared type — a Number field holding strings is the classic, usually from a form that posted before someone added a coercion.
  • Count values outside allowedValues, especially status enums that gained a value in code but not in the schema.
  • Look for fields present in the data that no schema mentions at all. These are either dead columns or an undocumented feature, and the difference matters.

We write this as a read-only script against a restored snapshot, never production, and we keep the output. It becomes the baseline for every later decision: which rules are real, which are aspirational, and which would break today if actually enforced.

The common finding is that the schema is stricter than the data. That is fine while the rules only run on new writes. It stops being fine the moment you consider enforcing them at the database level, or moving the data into a system with a real type checker.

What Meteor 3 changes

The Fibers removal reaches this layer in three specific places.

Hooks and custom validators that did database work. A custom validator that checked uniqueness with Collection.findOne(), or a collection-hooks before.insert that read another collection, was relying on synchronous Fiber-backed calls. Under Meteor 3 those calls are promise-returning. If the validator signature is synchronous, the check silently passes — it receives a pending promise and evaluates it as truthy — and the rule you thought was guarding your data stops guarding it without a single error in the logs. This is the failure mode worth grepping for first: search your schema files for findOne, find(, Meteor.call, and Meteor.userId, and review every hit by hand.

Package currency. The schema stack has moved. aldeed:simple-schema has an npm-published lineage (simpl-schema), and the Collection2 package has a maintained Meteor 3 line; matb33:collection-hooks has a successor as well. None of this is a rewrite — it is version work plus the async fixes above — but it does need to be on the migration plan explicitly, because these packages sit on the write path for the whole application. Check each one for a release that names Meteor 3 support before you assume it is fine.

autoValue ordering. autoValue functions that set createdAt, updatedAt, or a denormalized field run during cleaning, and anything asynchronous inside them needs the same treatment as a custom validator. Timestamps that quietly stop being set are unpleasant to discover later, because the data damage is invisible until somebody sorts by date.

None of these are hard fixes. They are just easy to miss, because a broken validator produces no error — it produces permissiveness.

Keep it, or move it?

Once the app is on Meteor 3, there is a real decision here, and we give a different answer depending on the app.

Keep SimpleSchema and Collection2 when the schemas are broadly accurate, forms are wired to them, and nobody is planning to leave Meteor soon. The stack is maintained, the team knows it, and rewriting a working validation layer buys nothing a customer can see. Most apps we assess stay here, and that is the right call.

Move the shape definitions to a runtime validator — Zod is the common choice, Valibot and TypeBox are reasonable — when you are adopting TypeScript, when validation needs to run somewhere other than a Meteor collection write (an HTTP endpoint, a worker, a queue consumer), or when a migration off Meteor is on the roadmap. A Zod schema is plain npm code with no Meteor coupling: it runs in a Next.js route handler exactly as it runs in a Meteor method, and it gives you a static type from the same definition. That portability is the whole argument. It is also why we prefer to do this during a strangler migration rather than as a standalone project — the new code needs validation anyway, and the schema is the natural thing to share across the boundary.

Add MongoDB-side validation ($jsonSchema on the collection) when writes reach the database from more than one place. This is the only rule that a rogue script cannot skip. Introduce it in validationAction: "warn" first, read the logs for a few weeks, fix what it finds, and only then move to error. Doing it the other way round takes an outage to learn the same lesson.

These are not exclusive. A sensible end state for an app mid-migration is Zod as the single definition, a thin adapter feeding the Meteor write path while it still exists, and a $jsonSchema floor on the two or three collections that matter most.

A sequence that does not break production

  1. Audit the data against the declared schemas; keep the numbers.
  2. Fix the async-unsafe validators, hooks, and autoValue functions as part of the Meteor 3 work. Add tests that assert a bad document is actually rejected — the whole risk here is rules that pass when they should fail.
  3. Reconcile schema and reality: either correct the data, or relax the rule to match what the business actually allows. Do not leave the gap undocumented.
  4. Decide keep-or-port on the criteria above, and write down why.
  5. If porting, do it one collection at a time, dual-running the old and new validators and logging disagreements before you remove anything.
  6. Consider a database-level floor on the collections whose integrity you would be called about at night.

Step 5 is the one people want to skip. It is also the one that turns a scary change into a boring one: for a week or two the new validator only observes, you read the disagreements, and by the time it takes over you already know it agrees with the old one.

Why we care about this one

The validation layer is the closest thing most Meteor apps have to written-down business rules. When a team inherits an application with no original authors, the schemas are frequently the best surviving documentation of what the data is supposed to mean — better than the wiki, better than the tests.

That makes it worth reading carefully before the Meteor 3 work touches it, and worth making portable if the app is eventually going somewhere else. Either way the goal is the same: the rules the code claims to enforce should be the rules it actually enforces.

If you are planning a Meteor 3 upgrade and the schema files have not been opened yet, open them. They usually have something to say.