Building a real compliance SaaS with Meteor 3.5 + Blaze 3.1 : no jQuery, 99% AI-written

Hi all,

I’m building a B2B SaaS on Meteor and I’d like to document the journey here, mostly the technical side: what I use, what works, what bit me, and what I miss :+1:

Most of what I post here is about making the framework itself better. But @xet7 keeps proving with WeKan that what gets built with Meteor is at least as interesting as what goes into it, so this time it’s my turn :wink:

This isn’t a benchmark app or an AI experiment: it’s a product intended to go into production with real customers and real HR data :nerd_face:

My idea is to post every couple of days/weeks/months (Idk) in this thread, usually around one real problem, its reproduction and fix, and a question for the community. Meteor Software people read this forum, and a few of the things below are probably worth upstream issues, so I’d rather share them than keep them buried in my notes :wink:

One thing upfront, because it changes how you should read everything else: about 99% of the application code is written by AI agents (Sorry @harry97 :hand_with_index_finger_and_thumb_crossed:)

Not by prompting “make me an app”, though. The process around that is probably as interesting as the resulting code, so consider this first post a preamble to a series within the thread: how I actually use AI to build this, with real issues, real failures, and eventually real transcripts :smile:

What the product does

It tracks mandatory certifications for industrial workforces: forklift licences, electrical clearances, first-aid, that kind of thing.

For each employee it answers two questions:

  • does this person hold everything their job and site require?
  • what expires soon?

Requirements come from the organisation: job, site, committee, or any combination of those.

Certificates come either from the customer’s training platform through an API connector, or from spreadsheet imports.

The target customers are industrial companies of any sector: wherever there are forklifts, electrical cabinets and an audit to pass.

The pilot customer has several thousand employees and a few thousand certificates. Nothing exotic in the domain; everything is in the details.

A couple of the companies I’ve talked to have already asked for an installable PWA on their employees’ phones, so a worker can show their certifications at any time, in front of a supervisor or an auditor. That’s on the roadmap, and one of the reasons I’ve been working on meteor create --pwa upstream (PR #14473).

How it’s built: 99% AI-written, 100% specified

I write very little application code by hand anymore. Like, over the last six months, probably only a few hundred lines.

Most of my work has moved upstream: architecture, specification, constraints, review, testing, and deciding what should exist in the first place.

The workflow is roughly this:

  • Brainstorming and scoping first. Before an implementation agent touches anything, I spend quite a lot of time shaping epics and issues. For that part, I currently prefer ChatGPT with GPT-5.6 Sol at max reasoning. I use it as a sparring partner: challenge the architecture, find missing cases, split something that is too large, question abstractions, and eventually turn the discussion into an issue precise enough that another agent can implement it without having to invent the product along the way.

    GPT-6 Astra dropped overnight, though, so I still need to properly test it. This paragraph may already be outdated :smile:

  • Extremely precise issues. Every piece of work is a GitHub issue, grouped in epics, that describes the slice end to end: data model, method, publication, screen, tests and documentation. A slice is never delivered one layer at a time. The issue says what is out of scope, which existing conventions apply, and what “done” means.

  • One agent implements using Claude Code, test first. It has to watch the test fail for the right reason before writing the implementation, then run the full suite and check that the total number of tests increased by the expected count.

  • A second model from another vendor reviews every change before merge. I use OpenAI’s Codex for that pass, giving it both the issue and the diff and asking it to look specifically for things the implementation model may be blind to because of its own habits.

  • Gemini audits the whole codebase once a day with fresh context and no conversation history: contradictions between code and documentation, drifting conventions, tests that don’t actually prove what they claim to prove, and similar structural issues. Useful findings become GitHub issues.

  • The architecture is mine, negotiated with the agents through discussions, issues and ADRs. When an agent proposes a structural change, it becomes an ADR first and code second.

There is another fairly important part of this setup: Meteor-specific skills.

I’ve been working on my own Meteor skills for roughly a year now, especially around Blaze development. They encode the kind of Meteor-specific knowledge that generic coding models either don’t know, know from an outdated version, or get subtly wrong: Blaze patterns, Tracker behaviour, publications, methods, package conventions, testing pitfalls, native DOM details, etc.

I don’t want all of that dumped permanently into a giant CLAUDE.md. Repository-specific rules still live there when they genuinely belong to the project, but reusable Meteor knowledge belongs in reusable skills that can be loaded when relevant.

I’m also following Nacho’s work on meteor/agent-skills very closely. I think this whole idea of giving agents good, reusable and progressively loaded framework knowledge is going to matter a lot for Meteor, especially because the framework has many conventions that are obvious to experienced Meteor developers but not necessarily obvious from reading one source file in isolation.

I’ll probably dedicate a post to the skills themselves at some point: what’s worth putting in one, what isn’t, how specific they should be, and whether they actually improve generated Meteor/Blaze code in practice.

Concretely, the AI setup is three subscriptions:

  • ChatGPT: mostly GPT-5.6 Sol at max reasoning for architecture brainstorming, epics and issue specification, plus Codex for the independent code review pass
  • Claude Max: Claude Code for implementation
  • Google AI Pro: Gemini for full-codebase audits

I’ll probably dedicate full posts later to what each one is actually good and bad at, because the differences are much more interesting than “model X is smarter than model Y”.

The safety net that makes all of this workable is below: real-browser tests, plus architecture guard tests that read the source code and fail when a convention is broken.

Without them, I would not trust generated code nearly as much.

One thing I’ve learned so far is that agents tend to fail silently in many of the same places Meteor fails silently. Most of the guards described further down exist because something broke once without the test suite telling me.

The stack

  • Meteor 3.5, currently pinned on 3.5.2-beta.0
  • Blaze 3.1.0-alpha.0, using the native DOM backend, with the jquery package removed entirely
  • Rspack as the bundler (rspack@1.3.0-beta352.0, @meteorjs/rspack)
  • accounts-password
  • the core roles package, for roles and permissions only; who a user is allowed to see is a home-grown layer next to it (see the questions at the end)
  • plain Mongo, no ORM
  • meteortesting:mocha with TEST_BROWSER_DRIVER=puppeteer, so client tests run in a real browser in-process, with the same fixtures as method and publication tests
  • Galaxy for hosting, with one app instance and one MongoDB database per customer
  • Astro Starlight for technical documentation, with ADRs for structural decisions

Three decisions people will probably want to argue about

Blaze, on purpose

This isn’t a legacy choice and it isn’t a compromise.

I’m a Blaze advocate, and I contributed a good part of the 3.1 alpha work, including the changes that made the native DOM mode possible :eyes:

For a data-heavy back-office made mostly of tables, forms, detail pages and reactive server data, I still think Blaze plus Tracker is exceptionally good :man_shrugging:

There’s almost no ceremony: no virtual DOM, no separate state library, templates that mostly read like the HTML they produce, and an event map that acts as the controller :+1:

It also turns out to be a very good target for AI agents :male_detective:

At least in my experience, they produce correct Blaze code faster and more consistently than complex React state management, provided the Meteor-specific pitfalls are documented and enforced :sunglasses:

That’s also one of the reasons I’ve spent so much time on the Blaze-focused parts of my Meteor skills: once the model has the right framework-specific context, Blaze is a surprisingly small and constrained target for an agent

Running the alpha in a real product is also the fastest way for me to find what’s still broken. When I do, I either report it or fix it upstream :smile:

Which brings me to the second decision.

No jQuery at all

Blaze 3.0 declares jquery as a weak dependency, but the DOM backend still throws jQuery not found at boot when it isn’t available.

Blaze 3.1 introduced the native DOM backend through PRs #493 and #497 on meteor/blaze.

That made the migration order important:

  1. Meteor 3.5
  2. Blaze 3.1
  3. remove jquery

Invert the last two and the client doesn’t boot :grimacing:

Everything in the application now uses native DOM APIs: find, findAll, querySelector, classList, dataset, closest, etc.

I also have a tiny CI guard that fails if any application file imports jQuery :face_with_monocle:

One nice detail: Blaze 3.1 prints a deprecation console.info when it detects jQuery. So during the migration, seeing that message proved the new backend was active, and its disappearance proved jQuery had actually been removed :smiling_face:

One database per customer, no clientId anywhere

Employee and compliance data are sensitive HR data :grimacing:

And French industrial customers are very protective of where that data lives. Before the first demo, every one of them asks the same question: where is it hosted? The answer has to be France, and I have to be able to show it rather than assert it. Production instances go to Galaxy’s eu-west-1 region, which is Roubaix, France, for both the app and its managed MongoDB, so the answer fits on one line of the contract. Every third-party service around them, email and file storage first, has to meet the same requirement, which is one more reason to keep their number small :grimacing:

I don’t want tenant isolation to depend on remembering a clientId filter in every publication, method and query.

With a shared database, one forgotten filter can become a cross-customer data leak.

So each customer gets:

  • one Galaxy application instance
  • one MongoDB database
  • its own settings and secrets
  • eventually its own subdomain and email sender configuration

Tenant isolation is structural rather than application-level.

What doesn’t multiply is the code. There is one GitHub repository, and every customer instance is a Galaxy app linked to that same repository through Galaxy’s Git integration: a merge on main redeploys all of them with the same build. A new customer is a new Galaxy app pointed at the same repo, with its own settings, its own database and, eventually, its own customer.<app>.app subdomain. The only thing that differs between instances is configuration, never code. With one pilot customer today, that’s the design, not yet a track record.

Migrations run at server startup, so each instance migrates its own database when it boots the new build. The fan-out is free; the trade-off is that a migration failing on one instance out of N has to be noticed, and provisioning a customer requires tooling.

I’ll write about that tooling once it exists in a form worth showing.

The pitfalls, short version

Each of these deserves its own post with a minimal reproduction.

They share one property: most of them fail silently, which is the worst possible failure mode in a test suite.

1. The test bundle can be empty

Without NODE_ENV=production, meteor test with Rspack could produce a bundle containing only a handful of tests, or sometimes zero, and still report:

0 passing

in green.

This is why I’m currently on 3.5.2-beta.0: it contains Rspack fixes around isolation between concurrent development and test builds.

I still count the tests after every run and verify that the total increases by the expected number when new tests are added.

It’s the only reliable check I’ve found so far.

2. A *.tests.js file that nobody imports never runs

And nothing tells you.

Every test file has to be explicitly imported from a client or server test barrel.

Forget the import and the suite remains perfectly green, just with one test file missing.

3. An unclosed tag can make a template disappear from the bundle

Again: no useful error.

The runtime symptom is simply:

Template.X is undefined

My render tests now begin with something like:

expect(Template.X).to.exist

before testing anything about the template itself.

4. blur and focus work without jQuery, but are surprisingly tricky to test

The native Blaze event backend aliases them to focusout and focusin, which bubble.

That works fine in the application.

But there are two misleading test cases:

  • a synthetic new Event('blur') triggers nothing, because Blaze isn’t actually listening for blur
  • the real browser gesture can also trigger nothing when document.hasFocus() is false, for example when the Puppeteer-controlled browser is in a background tab

Both failures look exactly like a bug in your application code.

Before debugging further, log document.hasFocus().

5. A click-swallowing bug in the native Blaze event backend

This is probably post 2.

With Blaze 3.1 alpha without jQuery, a wrapper element placed inside a {{#if}}, becoming the parent of an included template whose top-level content also begins with {{#if}}, can cause clicks inside the wrapper to be silently ignored.

The interesting part is why.

All {{#if}} views share the generic view name if, and the native backend’s in-scope heuristic compares view names. That can create a false positive and make the event backend decide that the event belongs to another view.

I have a minimal reproduction, and the issue is already open upstream as meteor/blaze#512, with a fix in PR #513 waiting for review.

6. this.added(...) + this.ready() is a snapshot, not a reactive publication

A low-level publication that manually calls this.added(...) and then this.ready() only publishes the initial value.

That’s perfectly valid if you intentionally want a snapshot.

But if the underlying value is expected to change, it becomes stale on the first write.

The confusing symptom is that the value you just typed appears briefly, then disappears again when the reactive client state catches up.

For those publications I’m now using observeChanges, plus proper cleanup in onStop.

Tests that read the source code

The part I’m probably happiest with is a small family of what I call architecture guards.

They’re tests that read the source tree itself and fail when an architectural convention is violated.

For example:

  • any Persons.find outside the single access-control module
  • any methods file that writes to the database without an audit log entry
  • any method or publication that server startup never imports
  • any CSS var(--something) referring to a theme token that isn’t declared
  • any routed page that doesn’t use the application’s standard page skeleton

Each guard exists because a real defect happened first.

The reason for that defect is documented at the top of the guard file, and exemptions are explicit and require a written reason.

Before adding a guard, I reintroduce the exact bug and watch the guard fail.

A guard that has never been seen red protects nothing.

This kind of testing has become particularly useful with AI-generated code, because it turns architectural conventions into executable constraints instead of relying on the model remembering a paragraph in a prompt.

What I miss

A few things have already made it onto my “Meteor should probably make this easier” list:

  • a way for meteor test to fail loudly when the resulting test bundle is empty
  • ideally, a way to detect test files that exist but are never imported
  • Blaze 3.1 out of alpha, with the native DOM backend mature enough to become the default
  • a way to see, from one place, that a startup migration went through on every instance of the same codebase (We really need to put that in the core imo)
  • a first-party story for per-instance secrets and email sender configuration on Galaxy. I already made the case for managed transactional email and object storage in this thread; with one instance per customer, every argument in it gets multiplied by N, including @allenfuller’s SOC 2 one :wink:

Some of these probably deserve upstream issues. As I work through this thread, I’ll open them with proper reproductions rather than just keeping a wishlist here.

Questions for you

Blaze 3.1 / native DOM: is anyone else running the alpha without jQuery in a real application? In particular, have you seen regressions around {{#each}} rendering or observe-sequence?

Rspack tests: have you seen incomplete or empty test bundles on Meteor 3.5 stable, or does this appear fixed in your setup?

Roles in real business applications: the core roles package answers “what can this user do?” very well: roles, permissions, hierarchy, scopes as plain context names. It doesn’t answer “on which employees?”, which is what our customers call a perimeter: one supervisor covers forklift drivers AND (Lyon OR Lille), another covers the Nantes site, same role, different perimeter. We lost a fair amount of hair trying to squeeze that into scopes before accepting that a perimeter belongs to the role assignment, not to the role, and that the application has to compute it. So: is anyone using roles as-is in a real business app with that kind of data-scoped access, or did you all end up writing your own layer next to it? I’m considering a tiny upstream extension, opaque application-defined metadata on a role assignment, and I’d like to know whether it would help anyone else.

Database-per-customer deployments: if you isolate customers by database or application instance, how are you handling provisioning, and how do you know a startup migration went through on every instance? Just scripts iterating over connection strings, or have you built something more structured?

Next posts will probably be:

  1. the {{#if}} native event backend click bug, with a minimal reproduction
  2. one concrete AI-built feature from issue text to merged slice, including the brainstorming/specification phase, what the implementation model got wrong, and what the review model caught
  3. eventually, a deeper look at the Meteor/Blaze skills I’m using to give the agents framework-specific knowledge

Thanks for reading and see you soon :partying_face: :man_dancing:

1 Like

Good experiment ahead with a promising production outcome!

Great that, in the meantime, you’re using the recently published Agent Skills as a form of validation. That input will be really important to keep improving them, and we’ll appreciate PRs around it with the evidence shown.

Rspack tests: have you seen incomplete or empty test bundles on Meteor 3.5 stable, or does this appear fixed in your setup?

Every test file has to be explicitly imported from a client or server test barrel.

Regarding tests, this depends specifically on how your app is configured. If it has a test entrypoint in the package.json file, it will need to be imported from a barrel index file. If that is omitted, it will run eagerly as Meteor has always done. This is also covered by E2E tests in Meteor core. This app for instance doesn’t have testModule described: meteor/tools/e2e-tests/apps/react-router/package.json at devel · meteor/meteor · GitHub

With Rstest, this will be improved further once a new standard for test discovery is enforced, but until then, Meteor’s way of handling them is preserved through the testModule config, with lazy or eager loading depending on its config.

If you find any issues with this, it may be good to cover them one by one in separate issues on the repository. Maybe it’s about updating the docs to make something clearer, improving the AI training, or finding an opportunity to further improve some scenarios.

99% of the application code is written by AI agents

NOOOOOOOOOOOOOOOOOOOO

It’s ok I use LLMs too :grin: just don’t tell anybody

1 Like

@dupontbertrand

Meteor Beta is old. There is now RC.

meteor update --release 3.5.2-rc.0

JQuery

WeKan uses JQuery. Sorry.

Multitenancy

For this, currently I have one Docker container, MongoDB user/password and database per user, at same MongoDB database:

Less Code

It’s possible deduplicate code, by combinining Blaze templates, CSS, etc. This also fixed many bugs, when there were differences at those templates, there is less code to maintain, etc.

Roles

WeKan does not yet use Meteor Roles. WeKan hardcodes roles, so that it is possible to have feature Impersonate User, logging of impersonation at Admin Panel Problems Impersonation, etc.