DEV Community

MongoDB Guests for MongoDB

Posted on

A Proven Application Layer for MongoDB

The successful alternative I used before Atlas Device SDKs. This Article was written by Raschid Jimenez.

Atlas Device SDKs reached end-of-life in September 2025, and it’s easy to understand why so many MongoDB developers liked them: direct client access, authentication, sync, and security, all tied closely to MongoDB Atlas.

Long before Atlas Device SDKs, I had already found a different way to get many of those same benefits. Using Parse Server and Back4app, I built production apps for years without the burden of maintaining custom API layers across every client and language. This article is the recipe I wish more MongoDB developers knew: why this stack works, what it gives you, and why it’s such a strong fit on top of MongoDB.

I will walk you through three things that sold me on Parse Server and Back4app: native client SDKs across every platform, row-level security for multi-tenant apps, and managed schema with index control. Along the way, I'll cover what else comes in the package: authentication, real-time updates via LiveQuery, file storage, auto-generated GraphQL APIs, and offline support. Data never leaves MongoDB Atlas. Back4app is a gateway that translates client requests into operations against your cluster.

            ┌──────────────────────────────────────┐
            │  Client (SDKs / REST / GraphQL)      │
            └────────────────────┬─────────────────┘
                                 │
            ┌────────────────────▼─────────────────┐
            │     Application Layer                │
            │       (Back4app - Stateless Gateway) │ 
            └────────────────────┬─────────────────┘
                                 │
            ┌────────────────────▼─────────────────┐
            │     MongoDB Atlas (your cluster)     │
            └──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Back4app Uses MongoDB Atlas as the Datastore

Back4app is a managed backend-as-a-service (BaaS) platform. Sign up, paste in your MongoDB connection string, and you get an application backend running in front of your cluster: APIs, SDKs, authentication, real-time sync, file storage, access control, and a dashboard. No provisioning servers, no configuring authorization, no maintaining the layer yourself.

The connection model is what should land for an Atlas audience: bring your own cluster. Back4app connects to an existing Atlas deployment via a standard MongoDB connection string. No migration, no copy of your data, no proprietary storage format, no lock-in at the storage tier. If you walked away from Back4app tomorrow, your Atlas cluster would still be there, untouched, with every native MongoDB primitive intact.

What Back4app adds is the application backend layer most teams end up building anyway: auto-generated REST and GraphQL APIs, authentication, real-time sync, file storage, ACLs and CLPs, and a visual data browser.

Back4app is a layer in front of Atlas, not a replacement for it.

Parse Server Under the Hood

Under the hood, Back4app runs Parse Server, an open-source backend framework with a mature SDK ecosystem across every major platform. That's where the Parse.Object, Parse.Query, Parse.ACL, and Parse.Schema APIs in the code samples throughout this article come from. They're Parse Server primitives. Back4app is the managed platform that runs Parse Server in production (like Atlas does for MongoDB), plus the pieces around it: hosted infrastructure, the connection-string flow into Atlas, the dashboard, the spreadsheet-like data browser, role-aware visual schema editing, monitoring, scaling, and backups.

The short version: code I write with a Parse.* prefix is Parse Server. The surfaces I interact with through the Back4app dashboard are Back4app.

Client SDKs Across Every Platform

With Atlas Device SDKs gone, the question for most teams is concrete: how do clients perform operations against MongoDB Atlas (CRUD, authentication, file uploads, real-time subscriptions, and access-controlled queries with relations) without building and maintaining a custom REST layer for each?

Back4app offers a set of native SDKs that talk to the gateway, which in turn talks to your Atlas cluster. Supported platforms:

  • iOS (Swift, Objective-C)
  • Android (Java, Kotlin)
  • Web (JavaScript, TypeScript)
  • React Native, Node.js
  • Flutter, Dart
  • PHP, .NET, C#
  • Edge runtimes

For anything not on that list, Back4app auto-generates REST and GraphQL endpoints from the same schema.

The interesting part isn't the language list. It's that the API shape is the same across all of them. Here are the four CRUD operations in JavaScript:

// CREATE - no REST endpoint, no schema migration, just save.
const order = new Parse.Object("Order");
order.set("total", 149.99);
await order.save();

// READ - ACLs are enforced server side; this user only sees their tenant's orders.
const query = new Parse.Query("Order");
query.greaterThan("total", 100);
query.include("customer"); // hydrate the pointer in one round trip
const orders = await query.find();

// UPDATE - same object, just mutate and save.
order.set("status", "shipped");
await order.save();

// DELETE - single call, ACL-checked at the data layer.
await order.destroy();
Enter fullscreen mode Exit fullscreen mode

That include("customer") line is worth looking at. On raw MongoDB, resolving a reference to fetch the related document means writing an aggregation pipeline:

// Without Parse - manual aggregation pipeline:
db.orders.aggregate([
  { $match: { total: { $gt: 100 } } },
  { $lookup: {
      from: "users",
      localField: "customer",
      foreignField: "_id",
      as: "customer"
  }},
  { $unwind: "$customer" }
]);

// With Parse - one line:
await new Parse.Query("Order")
  .greaterThan("total", 100)
  .include("customer")
  .find();
Enter fullscreen mode Exit fullscreen mode

Same result, one round trip, no pipeline to maintain. The include works identically across REST, GraphQL, and every SDK.

The same read in Swift, using the modern Parse-Swift SDK:

let query = Order.query("total" > 100)
  .include("customer")
let orders = try await query.find()
Enter fullscreen mode Exit fullscreen mode

A note on API symmetry: query.include("customer") reads the same in JavaScript and Swift. The naming follows each language's conventions, but the mental model is identical across platforms. Same query model, same ACL enforcement, same relational include semantics. A team shipping iOS, Android, and web doesn't re-architect the data layer per client. They translate one mental model into three idioms.

If GraphQL is the API you want, the same schema produces a typed GraphQL endpoint automatically. No resolvers to write, and the same ACLs and CLPs that govern the SDKs govern those queries too.

For reactive UIs, LiveQuery lets you subscribe to any query and receive push updates on create, update, and delete over WebSockets. No polling loops, no reconnection logic, no homegrown change-stream plumbing. Subscribe to a dashboard to live order updates, and the dashboard updates itself when an order changes upstream.

Row-Level Security and Multi-Tenancy

Multi-tenant access control is application-layer work by design: the database holds the data, the application decides who sees what. In practice, that means every team building multi-tenant SaaS on Atlas writes the same middleware: tenant filters injected into every query, role checks scattered across controllers, field redaction in serializers. That creates a lot of security-critical logic to maintain.

Back4app moves that enforcement down to the data layer. Four layers of access control work together:

Layer Granularity When to use
Application Keys Per client type Separate public, client, and master contexts
Class-Level Permissions Per class (collection) "Who can create or read Orders at all"
Access Control Lists Per document "Who can read this Order"
Protected / Field-level Per field isAdmin: only the server can write

Here's what each layer replaces in a hand-rolled implementation:

// Without Parse — middleware you write and never get to delete:
app.get("/api/invoices", auth, async (req, res) => {
  const invoices = await db.collection("invoices").find({
    tenantId: req.user.tenantId,        // remember this on every endpoint
    $or: [
      { ownerId: req.user._id },
      { sharedWith: req.user._id }
    ]
  }).toArray();
  // ...strip fields the role shouldn't see...
  res.json(invoices);
});

// With Parse — write zero filter middleware:
const invoices = await new Parse.Query("Invoice").find();
// ACLs and CLPs enforce the same rules across REST, GraphQL, and every SDK.
Enter fullscreen mode Exit fullscreen mode

Here's how you set those ACLs up for a multi-tenant invoice scoped to a tenant role:

const invoice = new Parse.Object("Invoice");
invoice.set("amount", 1200);

const acl = new Parse.ACL();
acl.setRoleReadAccess("tenant_acme", true);          // every user in this tenant
acl.setRoleWriteAccess("tenant_acme_admins", true);  // only admins can modify
invoice.setACL(acl);

await invoice.save();
Enter fullscreen mode Exit fullscreen mode

Any query from a user outside tenant_acme will not see this document. Enforcement happens server-side at the gateway, not in hopeful client code. The same filter applies whether the request comes from the iOS SDK, the GraphQL endpoint, or an inbound webhook. There is no client-side path that can bypass it.

When per-object rules aren't expressive enough ("users can update their own row, but only during business hours, and only if the invoice isn't locked"), Cloud Code provides server-side functions for custom validation:

// Cloud Code — server side function, runs in Parse Server, ACL-aware.
Parse.Cloud.beforeSave("Invoice", async (req) => {
  if (req.object.get("locked") && !req.master) {
    throw "Locked invoices cannot be modified";
  }
});
Enter fullscreen mode Exit fullscreen mode

Schema and Index Management

With Parse, the schema lives in the database itself. Every client SDK, REST endpoint, and GraphQL type derives from there automatically. One source of truth, propagated to every surface your app talks to.

Parse's ODM defines classes (collections) with typed fields: strings, numbers, dates, files, GeoPoints, pointers, and relations. Parse enforces types at the gateway across REST, GraphQL, and every SDK, so a malformed payload is rejected before it reaches Atlas. The underlying MongoDB collection and the indexes Parse needs are auto-created. Schema changes are tracked through the Parse schema API or the Back4app dashboard, and they propagate to every client without anyone regenerating models by hand.

Document stores don't usually feel relational, but Back4app adds pointers for one-to-one references and relations for many-to-many associations, with include queries that hydrate them in a single round trip:

const query = new Parse.Query("Order");
query.include("customer");   // hydrates the pointer
query.include("products");   // hydrates the relation
const orders = await query.find();
// One round trip. No manual $lookup pipeline.
Enter fullscreen mode Exit fullscreen mode

When you want schemas in version control rather than clicked into a dashboard, schemas can be declared programmatically, and migrations become regular code:

const schema = new Parse.Schema("Invoice");
schema
  .addString("customerEmail")
  .addNumber("amount")
  .addPointer("tenant", "Tenant")
  .addIndex("by_email", { customerEmail: 1 }); // explicit MongoDB index
await schema.save();
Enter fullscreen mode Exit fullscreen mode

Parse exposes MongoDB indexes directly. It creates defaults for ACL fields and pointers, but your hand-tuned indexes stay untouched. Parse only adds the ones it needs for ACL filtering and pointer fields. Your existing indexes for aggregation pipelines, Atlas Search, or hand-tuned queries stay exactly as you set them. The addIndex() call above creates a real MongoDB index. Parse doesn't abstract that away.

If you picked MongoDB for schemaless flexibility: Parse classes can stay flexible (allowAddField: true) during prototyping and tighten as the data model stabilizes. You pick per class. Strict where it matters, flexible where it helps.

Conclusion & Next Steps

Back4app is a stateless gateway in front of your existing MongoDB Atlas cluster. Connect it with a standard MongoDB connection string, and you get the dashboard, the spreadsheet-like data browser, SDKs across every major platform, auto-generated REST and GraphQL endpoints, LiveQuery for real-time UIs, and row-level security via CLPs and ACLs. Native MongoDB primitives (aggregations, transactions, indexes, Atlas Search) remain entirely available to you.

So my advice to you is: go to Back4App and sign up for free, create an app in your preferred language, and run your first operation on your data in under 10 minutes. Back4App offers a free tier (25,000 API requests per month), which is enough to try on a real app rather than a toy. My customers loved having their app backends hosted for free and paying only when they scaled.

Top comments (1)

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

The useful architectural invariant here is “all untrusted application traffic goes through the gateway.” ACLs and CLPs are only the effective tenant boundary while every other path is controlled. I’d keep a bypass inventory for direct Atlas clients, admin tooling, Cloud Code using master context, imports, webhooks, and background jobs—then test each against planted cross-tenant documents.

The regression suite should exercise the same deny cases through every surface: SDK, REST, GraphQL, and LiveQuery. Live subscriptions deserve special attention: role removal, token expiry, account disablement, and ACL changes should revoke or re-evaluate an existing stream, not only affect the next query. Master credentials should be isolated from client-reachable code and every master-context operation audited separately.

For schema changes, version ACL/CLP policy alongside migrations and test old plus new clients during rollout. I’d also validate generated queries with representative explain plans, because an authorization filter that is correct but unindexed can become a production incident at tenant scale.