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.
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
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
mintoo - ✓OpenAPI schema, with no annotations
- ✓Class-level permissions applied
- ✓Typed access —
product.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.
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)
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.
// 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
@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.
| NestJS | parse-server-kit | |
|---|---|---|
| Things you would otherwise build | ||
| Auth, sessions, password reset | you build it | built in |
| Users & roles | you build it | built in |
| Row-level permissions | you build it | built in |
| Real-time subscriptions | wire websockets | LiveQuery |
| File storage & adapters | you build it | built in |
| Push notifications | you build it | built in |
| Admin dashboard | build or buy | Parse Dashboard, one install |
| Things both do well | ||
| Schema & migrations | Prisma / TypeORM | from decorators |
| REST routing | @Controller | from method names |
| OpenAPI | @nestjs/swagger | automatic |
| Transactions | QueryRunner | @Transactional |
| Things NestJS does and this does not | ||
| Dependency injection | first class | none |
| Module boundaries | @Module | none |
| Guards / interceptors / pipes | full lifecycle | roles only |
| DTO validation | class-validator | field-level only |
| Database choice | anything | Mongo, Postgres with limits |
| Ecosystem & hiring pool | enormous | small |
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.
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.
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.
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.
- ›Getting started — three commands to a running API
- ›AI assistants — the rules, in the file your tool already reads
- ›Tutorial · a first API — models to scheduled jobs, in twenty minutes
- ›Tutorial · orders & stock — money, races and permissions that follow status
- ›Models & fields — every field type and option
- ›fromParams — request body to typed object, safely
- ›Permissions & ACL — the two layers, and five patterns
- ›Queries — filters, pointers, paging, counting
- ›Relationships — pointers, arrays and relations
- ›Users & auth — signup, sessions, roles, reset
- ›Files & images — uploads and the ACL cascade
- ›Endpoints — routing, validation, OpenAPI
- ›Triggers — all 21 types, and cron
- ›Real-time — LiveQuery, scoped by the same ACL
- ›Transactions — atomic writes across the call
- ›Optimistic locking — one line against lost updates
- ›Seeding data — roles and a first admin, re-runnably
- ›Parse Dashboard — an admin console, one install away
- ›Errors — codes, throwing, what clients see
- ›Troubleshooting — symptom to page
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.
- ›parseplatform.org — the project, its SDKs and its community
- ›Parse docs — the reference for every Parse concept
- ›parse-community/parse-server — the server itself
- ›parse-dashboard — the admin console, one install away
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.