A working backend for startups, hackathons, sales pitches in an afternoon.

Parse Server already gives you auth, sessions, roles, row-level permissions, real-time subscriptions and file storage. What it never gave you was a pleasant way to write against it. This is that layer — so the first month goes into your idea instead of your plumbing.

Get started Read the documentation $ npx parse-server-kit new my-api

Who this is for

Your first version should cost days, not months

This is built for the early stage: a startup racing to a first release, an MVP that has to exist by Friday, a hackathon where the clock is the whole problem, a sales pitch that needs a live demo rather than slides. At that point the thing that decides whether it happens is how quickly you can put it in front of someone — not your architecture, and not which ORM you picked. The backend is almost never the interesting part of the idea, and it is almost always where the first month goes.

So the position here is narrow and specific: you should never write authentication, sessions, roles, row-level permissions, file storage or real-time again. That is plumbing every product needs and no product competes on. Write the part that is actually yours.

A good fit

  • An idea you need in front of users this month
  • A demo that has to work in front of a customer
  • A product with accounts, roles and per-user data
  • A mobile or web client that needs an API now
  • An internal tool that has to be right, not novel
  • A small team with no time to run a platform

Look elsewhere

  • ·The backend is the product — a database, a queue, an engine
  • ·Mostly domain logic over a schema you do not own
  • ·A large team that needs enforced module boundaries
  • ·Postgres-first, with transactions and locking that matter
Starting fast is not the same as a low ceiling Parse Server is not a toy you graduate from. It began at Parse, was open-sourced in 2016, and has been run in production by companies of every size since. Sharding, read replicas, a separate LiveQuery server and horizontal scaling are all normal operations, not rewrites. Choosing it to move quickly does not mean choosing it temporarily.

The idea

One class is the whole definition

Declare a model once. The database schema, a unique index, a MongoDB validator, the class-level permissions and the OpenAPI schema are all derived from it — there is no migration to write and no DTO to keep in step.

@ParseField maps each property onto Parse's attribute store, so product.price is the supported API — not product.get('price').

Options are checked when the file is imported, not when a request arrives: a Pointer without a targetClass, min on a String, an invalid regex — all throw at boot.

@ParseClass('Product', {
  clp: { find: { requiresAuthentication: true } },
})
export default class Product extends BaseModel {

  @ParseField({ type: 'String', required: true })
  declare name: string;

  @ParseField({ type: 'String', unique: true })
  declare sku: string;

  @ParseField({ type: 'Number', min: 0 })
  declare price: number;
}
  • Schema generated from the decorators
  • Unique index on sku, created at boot
  • MongoDB validator so the database enforces min too
  • OpenAPI schema, with no annotations
  • Class-level permissions applied
  • Typed accessproduct.price, not .get()

Written with an assistant

There is so little of it to write that an AI can write most of it

Look at what you just read. The schema, the index, the validator, the permissions and the OpenAPI schema are one class. An assistant cannot leave five things out of step when there is only one thing.

That is the whole argument, and it is a mechanical one rather than a claim about any particular model. On a typical stack a new entity means keeping a migration, an entity class, a DTO, a validator and a controller consistent by hand — five files that drift from each other, which is exactly the kind of work AI is worst at. Here it is two files, generated with the edit points already marked.

And the traps are written down. This library has a handful of ways to fail silently — a shadowed field, a reversed decorator pair — and they are the ones a model walks into by default, because the wrong form is correct in every other library.

So psk asks which assistant you use and writes the rules into the file it already reads: Claude Code, Cursor, Copilot, Windsurf, Gemini, or AGENTS.md for everything else. Claude Code also gets two skills and a reviewer that hunts exactly those mistakes.

How the assistant setup works →

  Which AI coding assistants do you use?

    1  Claude Code       CLAUDE.md + skills + agent
     2  Cursor            .cursor/rules/
     3  GitHub Copilot    .github/
     4  Windsurf          .windsurf/rules/
     5  Gemini CLI        GEMINI.md
     6  AGENTS.md         the cross-tool convention
     0  none

  Numbers, comma separated (6)
Being honest about what that buys you Instructions raise the floor; they do not remove the need to read the diff. What makes the difference here is that the surface is small enough to hold in one head — yours or a model's — and that every way it fails quietly is written down rather than discovered. Speed is the promise; the troubleshooting page is the proof it was earned honestly.

Why Parse Server

Authorisation the database enforces, not your endpoints

Two layers. Class-level permissions decide who may touch a class at all. An ACL on every row decides who may touch that particular record — and the database applies it to every query, whether or not the endpoint remembered to.

Set the ACL when the record is created, and the filtering stops being your problem. listNotes needs no equalTo('owner', user): pass the session token and only permitted rows come back.

There is no if (doc.ownerId !== user.id) throw to forget in the twelfth endpoint, no filter to omit from a new query, and no admin panel quietly reading everything because it was written last.

The full guide to CLP and ACLs →

// once, when the record is created
note.setACL(implementACL({
  owner: [{ user: user.id, read: true, write: true }],
  roleRules: [{ role: 'Admin', read: true, write: true }],
  publicRead: status === 'published',
}));

// every query afterwards is filtered for free
const mine = await new Parse.Query(Note)
  .find({ sessionToken });   // no where clause needed

Routing

The method name is the route

No route table, no controller decorators to keep in sync. Rename the method and the route follows. Matching is done against the class's real method list, so getProduct and getProductCategory cannot collide.

/classes, /schemas and /batch are blocked by middleware, so clients only reach the endpoints you declare.

@Route(Product)
class ProductFunctions {

  @CloudFunction({ methods: ['POST'] })
  static async createProduct(req) { … }
  // → POST /api/products/createProduct

  @CloudFunction({ methods: ['GET'] })
  static async listProducts(req) { … }
  // → GET  /api/products/listProducts
}

Concurrency

Two guards most Parse projects go without

Transactions that follow the call. Parse Server keeps its open session on a shared controller, so one cloud function's transaction swallows every unrelated request running at the same moment. This one lives in AsyncLocalStorage: every save, destroy and query inside the body joins automatically, and two callers never see each other's.

Optimistic locking in one line. Every read carries the version it was read at; every save asserts it. The adapter moves the assertion into the write's filter and increments the field, so a stale save is refused rather than silently overwriting.

@CloudFunction({ methods: ['POST'] })  // above
@Transactional()                        // below
static async placeOrder(req) {
  await order.save(null, { useMasterKey: true });
  await stock.save(null, { useMasterKey: true });
}  // both land, or neither


class Job extends BaseModel {
  @ParseVersionField()
  declare version: number;
}  // that is the entire feature
Order used to matter here. It no longer does. @CloudFunction captured the method the moment it was applied, so writing @Transactional() above it meant the registry kept the unwrapped method and the transaction silently never opened. The registry now re-reads the method when it registers it, so either order works. That is the pattern throughout: where a trap can be removed rather than documented, it was.

Honestly

Compared with NestJS

These are not really competitors, and it is worth being precise about why. NestJS is an application framework: it hands you structure — modules, DI, guards, pipes — and you build the backend inside it. Parse Server is a backend: users, sessions, permissions, files, and real-time already exist, and you customise what they do. This library is the layer that makes customising it feel like writing TypeScript instead of writing configuration.

So the question is not "which framework is better". It is how much of your backend is the part Parse already wrote. For a product with accounts, roles, per-row visibility, uploads, and a mobile client, that share is large and starting from Parse saves months. For a service that is mostly domain logic over an existing database, that share is near zero and NestJS wins outright.

NestJSparse-server-kit
Things you would otherwise build
Auth, sessions, password resetyou build itbuilt in
Users & rolesyou build itbuilt in
Row-level permissionsyou build itbuilt in
Real-time subscriptionswire websocketsLiveQuery
File storage & adaptersyou build itbuilt in
Push notificationsyou build itbuilt in
Admin dashboardbuild or buyParse Dashboard, one install
Things both do well
Schema & migrationsPrisma / TypeORMfrom decorators
REST routing@Controllerfrom method names
OpenAPI@nestjs/swaggerautomatic
TransactionsQueryRunner@Transactional
Things NestJS does and this does not
Dependency injectionfirst classnone
Module boundaries@Modulenone
Guards / interceptors / pipesfull lifecycleroles only
DTO validationclass-validatorfield-level only
Database choiceanythingMongo, Postgres with limits
Ecosystem & hiring poolenormoussmall

The same rule, both ways

"Users may only see their own orders" — the difference that never stops costing you.

// NestJS — the filter lives in every query
findAll(user) {
  return this.repo.find({
    where: { ownerId: user.id },   // remember it
  });
}

async findOne(id, user) {
  const o = await this.repo.findOneBy({ id });
  if (o.ownerId !== user.id)         // and here
    throw new ForbiddenException();
  return o;
}

// …and in the report endpoint, the export
// job, and the admin screen added next
// quarter by someone else
// parse-server-kit — written once,
// when the row is created
order.setACL(implementACL({
  owner: [{ user: user.id,
            read: true, write: true }],
}));


// every query afterwards, everywhere:
await new Parse.Query(Order)
  .find({ sessionToken });

// no where clause. The database applies
// the rule whether or not the endpoint
// remembered to.
A fair way to decide Write down what your backend must do, then cross out everything Parse already provides. If what remains is small, this is a shortcut worth taking. If what remains is most of the list — or if it is the architecture itself you need — NestJS is the better tool, and this page is not trying to talk you out of it.

The full comparison, with code for auth and CRUD →

Where the effort went

Most of the work is in what fails quietly

A decorator that does nothing, a transaction that never opens, a field that reads back correctly but never reaches the database — none of these raise an error. Two have been made impossible, four now report themselves at boot, and the rest are written down.

206tests, unit and integration
0runtime dependencies
2–11×faster body parsing
21Parse trigger types

The integration suite boots a real Parse Server against MongoDB and exercises versioned saves, stale-save conflicts and transaction rollback end to end — because the transaction layer leans on parse-server internals, and an upstream release can move them.

Read the troubleshooting guide →

Documentation

Everything you need, without leaving

Parse concepts link out to the Parse Platform docs where the detail belongs. Everything you reach for while building is here.

Credit where it is due

Built on Parse Platform

Nearly everything this page calls a feature — users, sessions, roles, class-level permissions, row ACLs, LiveQuery, file adapters, push, the admin dashboard — is Parse Platform's work, not ours. Parse Server began at Parse, was open-sourced in 2016 when the hosted service closed, and has been maintained by its community ever since. It is a mature, actively released project with SDKs for the web, iOS, Android, Flutter and more.

This library is a layer on top of it. It does not fork Parse Server, replace it, or wrap it in an abstraction you then have to learn instead. It adds decorators, routing, schema generation and a CLI, and gets out of the way — a Parse concept behaves here exactly as the Parse documentation says it does, which is why these pages link out to it rather than paraphrasing.

Start with a project that already runs

The generator writes the correct boot order, a Docker MongoDB configured as a replica set so transactions work, and a seeded user so your first request succeeds.