Inspiration

You move to a new city for school or work. You find the apartment, the grocery store, the bus route. Then Friday comes and you don't know which mosque to pray at. And once you get there, you still don't know anyone, because a community's actual life doesn't happen in the prayer hall. It happens in a WhatsApp group nobody added you to.

Both of us pray at Khadija. Here's what that looks like from three different chairs.

If you're new, everything is invisible. The halaqa on Wednesday, the sisters' study circle, the food drive that needed eight people. All of it is happening, and none of it reaches you. You pray Isha and go home. Six months later you find out there was a whole community standing right next to you.

If you want to help, there's nowhere to look. "Who needs volunteers?" has no answer you can search for. You have to already be on the inside to know that iftar setup this Saturday is short four people.

And if you're the coordinator, you are the bottleneck. You post to Instagram, you post to WhatsApp, the mosque website notifies nobody, and at Madina the announcement is a sheet of paper on a corkboard. Then you count heads from memory and hope enough people turn up.

So the problem isn't that mosques aren't doing anything. Every mosque is busy. The problem is that a mosque's activity is only visible to people already deep inside it, which is exactly backwards, because the people who need it most are the ones who just arrived.

We did check whether someone had already solved this. Seven mosque products advertise themselves as the thing that replaces WhatsApp. Our mosques are still on WhatsApp. So it was never a missing-software problem. It's that nothing built for mosques actually gets adopted by one, because it keeps getting built by people who don't attend a mosque. We do. That's the whole reason we built it.

What it does

M'Ensemble is three apps that share one database and one contract.

1. The member app

This is the one a newcomer uses.

  • A city view. Open the app, it works out which city you're in, and shows you every mosque we cover in Montréal, Québec, Ottawa and Toronto, and otehr cities on a map, with a count for each. You can browse a city you haven't moved to yet. Looking around is free. The gate is on signing up for something, not on seeing what exists.
  • One feed across every mosque you follow. Events, classes, volunteer calls and announcements, filterable by type, each card badged with the mosque it came from. Not four Instagram accounts and a group chat.
  • Claim a shift in two taps. A volunteer post shows 3 of 6 filled. You take a slot. That's the whole flow. No reply-all, no "I'll take it" buried forty messages down.
  • Push that's filtered by what you actually care about. You pick your interests when you sign up: volunteering, community meals, youth, sisters, facilities, outreach, education, fundraising. A post only reaches people who follow that mosque, care about that category, and haven't muted that type. Nobody gets spammed into turning notifications off, which is how every mosque group chat eventually dies.
  • Prayer times that are actually your mosque's. Adhan times get computed right on the device from the mosque's coordinates, calculation method and madhab, so they work with no signal at all. But adhan isn't when people pray. Iqamah is, and iqamah is a decision a specific building makes. So coordinators enter theirs: a fixed time like 20:30, or an offset like +15 min after Maghrib, with an effective-from date and separate Jummah sessions. The app shows you both, plus the Hijri date and a live countdown to the next prayer.
  • A record of your service. Your upcoming commitments, your past ones, and your total hours. That last one turned out to matter a lot to students, who need documented volunteer hours anyway.
  • English, French and Arabic on every screen, with proper RTL. Not a half-translation with English leaking through the gaps: a missing key is a compile error.

2. The coordinator app

Same codebase, completely different app. Sign in as a coordinator and you get your own tab bar, your own screens, and a mosque switcher if you run more than one.

  • A dashboard that leads with what's broken. Which shifts are unfilled, what's coming up, who just signed up.
  • A coverage screen. Filled against needed, the actual list of names, and a check-in button next to each one. This is the screen that ends the WhatsApp comparison, because WhatsApp can't tell you whether Saturday is covered.
  • QR check-in. The coordinator puts a code on screen, volunteers scan it when they arrive. Manual and bulk check-in are right there as a fallback, because the wifi in a mosque basement is not something you want to depend on.
  • A member directory with a track record. Who signed up, who actually showed up, who you can count on. Plus promote and demote.
  • Post creation that changes shape with the type. Slots for a volunteer call, capacity for an event, recurring sessions for a class, and a poster image on any of them.

3. The super admin console

This is the third app, and it's a separate web app rather than anything a phone ever sees. Two mosques is a demo. Two hundred is a product, and a product needs somebody who can actually run it.

Before this existed, making someone a coordinator meant adding their email to a constant in the source and shipping a new build. That's a fine hack at hour six of a hackathon and completely untenable after. So the console does the real version:

  • Provisioning. Create a mosque and mint its coordinator's account in a single action. The server generates the password, hands it back exactly once on a credential slip you can copy, and never stores it in the clear or writes it to a log. Losing it means resetting it, not looking it up.
  • An overview of the platform: mosques, users, posts, signups, attendance, each with its trend against the period before, and charts of daily activity.
  • User administration. Search every account, see the mosques they hold a role at and what they've signed up for, change a membership role, grant or remove a platform role, suspend an account, reset a password. Suspension is reversible and keeps every row the person ever created, because deleting a user would orphan their signups and quietly change six mosques' attendance numbers.
  • Billing. Plans and subscriptions per mosque, invoices with line items, payments recorded against them, and donations tracked with our fee split kept separate from the mosque's net.
  • Partners and campaigns. Local businesses, their campaigns, and the approval queue for them.
  • Support. A ticket inbox with threaded replies, so "the app is broken" has somewhere to go that isn't one person's DMs.
  • An audit log. Append-only, covering every privileged action, because "who suspended this account" needs an answer that doesn't depend on anyone's memory.

Two things about it that we care about more than the feature list.

The console has its own permission tier, and it isn't the mosque one. Membership.role makes you an admin at a specific mosque. User.platformRole is what gets you into the console at all, and they're deliberately different fields. Being a coordinator at six mosques still grants you exactly nothing here. On top of that the console splits into support and superadmin: support can read everything and work the ticket queue, but only a superadmin can move money, issue mosque credentials, or hand out a platform role. That split exists so a volunteer answering the inbox is never one misclick from voiding an invoice.

On ads, the reader is never the product. We wrote that rule down before we wrote the code. Nothing here talks to Meta or Google. The creative sits in our own database, targeting is matched against our own rows (city, mosque, declared interest), and what goes down to the phone carries no commercial payload at all. We keep no per-person impression record. The smallest thing this system counts is one campaign on one day. A mosque has to be able to promise its members what they will and won't be shown, and we wanted that promise to be true in the schema, not just in a privacy policy.

How we built it

A TypeScript monorepo, four workspaces, one contract.

packages/shared/   the contract: types, the MEnsembleApi interface, fixtures
apps/mobile/       Expo + expo-router, member shell and coordinator shell
apps/server/       Express + MongoDB + Mongoose
apps/admin/        React + Vite, the super admin console

We spent the first hour writing no code at all. We locked the type definitions and the API interface in packages/shared and agreed that changing either one took a PR. Four people running AI coding agents against a schema that keeps moving is how a team loses six hours at 3am. We'd rather lose one hour deliberately.

Then we built the entire frontend against mocks. One rule made that safe: no screen ever calls fetch. Everything goes through src/api/client.ts, which in phase one just returned in-memory fixtures (two real mosques, nineteen realistic posts, twenty members) and in phase four became a real HTTP client with identical signatures. Swapping over was a boolean and a base URL. We kept the mock client afterwards as the reference implementation, so when the app and the server disagreed about a shape, the mock was the tiebreaker.

That's what let three people build member screens while a fourth built the whole coordinator shell, at the same time, on real phones, with no backend in existence yet. By hour eight the app was fully demoable on mocks in all three languages. That checkpoint is the reason we shipped anything.

The server is Express and Mongoose with Zod on every request body, JWT auth, and guards that stack: requireAuth first, then requireAdmin(mosqueId), then requirePlatform and requireSuperAdmin for the console. Follow (public, drives the feed) is a different collection from Membership (role, drives permissions), and keeping them apart is what stops you from accidentally handing admin rights to someone who merely follows a mosque.

The console is React and Vite, sharing the same packages/shared types as everything else, so a change to the invoice shape breaks the console's build in the same commit it breaks the server's. It talks to /api/admin/*, which is mounted behind the platform guards and nothing else.

Maps are OpenStreetMap through Leaflet inside a WebView. No API key, no native map SDK, no billing account. It behaves identically in Expo Go on both platforms and in a standalone build, and falls back to a list on web. For a 24-hour build, "works everywhere with zero setup" beat every richer option we looked at.

Prayer maths runs on the device using the adhan library, so prayer times work with no network and no backend whatsoever. The pure half of that then moved into packages/shared so the server computes the same table from the same code. The app and the API physically cannot disagree about Fajr.

Push is Expo's service, fanned out when a post is created, filtered by follow, then interest, then notification preference. Every push failure gets logged and swallowed. A notification that doesn't land must never fail the thing that caused it.

Challenges we ran into

The last slot. Two people tap "sign up" for the final spot at the same moment. Read-then-write gives you 7 of 6 filled and two volunteers who both think they're confirmed. The fix is to never read first. One atomic conditional update, plus a unique compound index on {postId, userId} so a double-tap can't produce two rows either:

const post = await Post.findOneAndUpdate(
  { _id: postId, $expr: { $lt: ['$slotsFilled', '$slotsNeeded'] } },
  { $inc: { slotsFilled: 1 } },
  { new: true },
);
if (!post) throw new HttpError(409, ERROR.POST_FULL);

There's a vitest case that fires concurrent claims at the last slot and asserts exactly one of them wins. We also ran it on two physical phones, which is a genuinely satisfying thing to watch break before you fix it.

Daylight saving almost moved every mosque by an hour. Iqamah is 20:30. That's a wall-clock fact about a building, not a moment in time. Store it as UTC and the first Sunday in November silently shifts every mosque in the app. So we store the string. A time a human wrote on a whiteboard just isn't the same kind of thing as a time a machine recorded, and it took us a while to say that out loud.

Arabic isn't a translation, it's a layout. Adding ar meant every row direction, every icon position, every text alignment, and a font stack that renders Arabic properly on both platforms. We ended up passing align, row and font out of the same hook that gives a screen its strings, so direction arrives the same way text does. Bolting RTL on afterwards would have been a rewrite. Doing it during the polish phase was still the most expensive "small" task of the whole build.

"Which city is this mosque in?" got written three times. Once in the app, once in the console, once in ad targeting. Then they started giving three different Montréal counts. We deleted all three and put one nearest-centre function in packages/shared. Nothing in the schema carries a city field at all: a mosque is simply nearest to some centre, so adding a city is adding a row instead of running a migration. Laval and Longueuil resolve to Montréal on purpose, because a Montréal volunteer really does drive out to a Laval iftar.

Building the console without wrecking the phone app. The platform layer touches User, adds nine collections, and introduces a whole second permission system. Doing that on a codebase four people are actively shipping to could easily have broken the demo. It didn't, because the platform types live in their own file, the routes live behind their own guard, and nothing on the phone reads any of it. Everything the console needs is either a new collection or an additive field.

Scope. The roadmap (tax receipts, parent portal, facilities booking, janazah coordination, lobby TV displays) got written down at hour zero explicitly as the list of things we were not building. Writing them down is what made it possible to walk past them at hour fifteen.

What we learned

The contract was worth more than any single feature. Locking the types and the API interface before writing a line let four people work in parallel for eight hours and then integrate in three. Every hour we've ever lost at a hackathon has been an integration hour.

Mock-first isn't a shortcut, it's a design constraint. Because the app was fully usable before a server existed, the API ended up shaped like what the screens actually needed, instead of the screens getting bent around whatever the ORM made convenient.

Test the scary thing at hour three, not hour eighteen. Push notifications don't work on simulators. Whatever you're going to find out at 3am, find it out at noon instead.

Permissions are a data model problem, not a UI problem. The moment we had three kinds of user, hiding a button stopped being an answer. Coordinator access and platform access had to be different fields on different objects with different guards, and getting that wrong wouldn't have shown up until somebody got into something they shouldn't.

Adoption is a product feature, not a marketing one. Mosques don't stay on WhatsApp because WhatsApp is good. They stay because switching means one volunteer coordinator has to migrate an entire community by themselves. So the app has to work when only some people have it. That's why the feed stays browsable, why a coordinator can bulk-check-in people who never installed anything, and why there's a free tier for small musallahs from day one.

Building for a place you actually belong to changes what you build. We never had to guess what a coordinator needs, and we never had to invent the frustrating parts. We already knew adhan time isn't when people pray. That's a five-second detail no amount of market research would have surfaced, and it's the first thing every coordinator we showed this to reacted to.

What's next

  • Real payment processing. Donations right now are recorded, not charged: rows entered and settled by hand, with feeCents and netCents both stored so that changing a fee schedule next quarter can never retroactively alter what a mosque was owed last quarter. Connecting a processor writes the same rows, which is why method and reference already exist.
  • Self-serve mosque onboarding. Provisioning goes through us today. It should be a form a mosque fills in, with the console reviewing rather than typing.
  • Onboard the two mosques we actually attend. Not a pilot pitched to strangers. Khadija and Madina, where we pray.
  • The roadmap we deliberately didn't build: tax receipts, a weekend-school parent portal, facilities booking, janazah coordination, lobby TV displays, cross-city feed.

Honestly, what's real and what isn't

A demo should say so, so here it is.

Real, end to end: auth and JWT; the feed; post creation for all four types; the atomic signup and withdrawal; QR and manual check-in; service hours; prayer times and iqamah config; the coordinator dashboard, coverage, roster and member directory; push fan-out through Expo; poster upload; likes; EN, FR and AR with RTL; OpenStreetMap; the full REST API including the whole /api/admin surface, with a vitest suite that covers the concurrency race.

Real in the console: sign-in with platform roles, the overview, mosques, provisioning and the credential slip, users, and billing. The rest of the console's screens (partners, campaigns, support, events, audit log) sit on finished API endpoints and are the last thing being wired up.

Not real: no money moves. No processor is connected and no ad campaign has ever been sold to anyone. Those models exist because we wanted the schema to be honest about where this goes, not because it's running today.

Built With

Share this project:

Updates

Submission history