From 510ac8799345f39f261fcc803ef0f0ca06f3a7cb Mon Sep 17 00:00:00 2001 From: Waseem Alhalabi <217769131+walhalabi-code@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:24:42 +0300 Subject: [PATCH] Add clientWritable and paginate Two features, both closing failures that run perfectly while being wrong. clientWritable: false on @ParseField. fromParams set every declared field the request carried - mass assignment. An endpoint accepting {"title"} also accepted {"title", "status": "published", "views": 9999}, and the row saved with values the caller was never meant to choose, silently. Marked on the model, the rule holds wherever fromParams is used instead of depending on every endpoint overwriting afterwards. The first implementation was fake, and the tests caught it: filtering inside the field loop came after Parse.Object.fromJSON had already assigned every key. The filter now runs on the source before the object is built. Discarding is silent to the CLIENT on purpose - refusing the request would tell an attacker which fields are protected, and would break a merely stale app. The developer is the one silence serves badly, so outside production a warning names the field, once per class and field. paginate(query, params, options). Every list endpoint hand-writes the same seven lines, and the obvious version returns results.length as the count - the size of the page the client already has, which nothing can paginate with. It shipped in this library's own starter. paginate reads limit and skip as the strings a GET sends, caps the limit, uses withCount for the true total, and computes hasMore. It also appends a createdAt tiebreaker to the sort. "Always sort" is not sufficient advice: sorting by a low-cardinality column leaves ties, and tied rows page exactly as badly as unsorted ones - repeated on two pages or dropped between them, with nothing reported. Appended, never overriding, so the endpoint still owns the order; tiebreak: false opts out for the caller whose indexes make the extra key expensive. The starter uses both: views is now server-owned, and listNotes shrinks to three lines of intent. RULES.md gains rule 7 and rewrites rule 6 from "overwrite after the call" to the declaration; the endpoint and model skills, CLAUDE.md, from-params.html and queries.html all follow, with search index entries. 356 tests pass, 21 of them new. --- CLAUDE.md | 46 +++++++ docs/assets/search-index.js | 2 +- docs/assets/search-index.json | 2 +- docs/guide/from-params.html | 39 +++++- docs/guide/queries.html | 36 ++++++ package-lock.json | 4 +- package.json | 2 +- src/decorators/parseDecorators.ts | 35 ++++- src/global.d.ts | 4 + src/index.ts | 2 + src/models/BaseModel.ts | 73 ++++++++++- src/utils/pagination.ts | 163 ++++++++++++++++++++++++ templates/ai/RULES.md | 56 ++++++-- templates/ai/skills/parse-endpoint.md | 19 ++- templates/ai/skills/parse-model.md | 4 +- templates/default/src/functions/note.ts | 52 +++----- templates/default/src/models/Note.ts | 10 +- test/clientWritable.test.ts | 100 +++++++++++++++ test/pagination.test.ts | 133 +++++++++++++++++++ 19 files changed, 718 insertions(+), 64 deletions(-) create mode 100644 src/utils/pagination.ts create mode 100644 test/clientWritable.test.ts create mode 100644 test/pagination.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index a6b20b3..9d0d57d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,6 +244,7 @@ interface ParseFieldOptions { pattern?: string; // String only, valid RegExp geo?: boolean; // GeoPoint only → 2dsphere index ttlSeconds?: number; // Date only → TTL index + clientWritable?: boolean; // default true; false = fromParams ignores it index?: boolean | 1 | -1; // mutually exclusive with `unique` unique?: boolean; // mutually exclusive with `index` } @@ -274,6 +275,27 @@ class BaseModel extends Parse.Object { **skipped** (`IMG`, `File` need their own handling). - An Array field converts to pointers **only if `targetClass` is set**. - A Pointer given `null` or `{}` is set to `null` (explicit clear). + - Fields declared `clientWritable: false` are **skipped**, whatever the body + contains — see below. + +**`fromParams` sets every declared field the request carries.** That is mass +assignment: an endpoint accepting `{"title": "…"}` also accepts +`{"title": "…", "status": "published", "views": 9999}`, and the row saves with +values the caller was never meant to choose. Nothing throws; nothing is logged. + +Mark server-owned fields on the model and the rule holds wherever `fromParams` +is used, rather than depending on every endpoint remembering to overwrite them: + +```ts +@ParseField({type: 'Number', min: 0, clientWritable: false}) +declare views: number; + +note.views += 1; // your code — unaffected +Note.fromParams(req.params); // `views` in the body is discarded +``` + +It is the mirror of Parse's `protectedFields`, which hides fields on the way +**out**; this refuses them on the way **in**. `Parse.Query` returns `Parse.Object`, not your typed class — cast the result (`row as Product`) or direct property access is unavailable. @@ -720,8 +742,32 @@ function generateRandomString(length?: number): string; function generateRandomInteger(length: number): string; function sleep(ms: number): Promise; function formatCount(num: any, locale?: string): string; // compact notation + +function paginate( + query: Parse.Query, + params: {limit?: unknown; skip?: unknown}, + options?: {defaultLimit?: number; maxLimit?: number; useMasterKey?: boolean; sessionToken?: string} +): Promise<{results: T[]; count: number; limit: number; skip: number; hasMore: boolean}>; ``` +**`paginate` is how to write a list endpoint.** It reads `limit` and `skip` as +the strings a GET sends, caps the limit (`MAX_QUERY_LIMIT` by default), calls +`withCount()` so `count` is the **total matching rows** rather than the size of +this page, and computes `hasMore`. + +```ts +const query = new Parse.Query(Product).descending('createdAt'); +return paginate(query, req.params, {useMasterKey: true}); +``` + +The hand-written version returns `results.length` as the count — the length of +an array the client already has, which no paginator can use — and it runs +perfectly while doing so. + +**Sort the query yourself.** `paginate` does not choose an order, because the +right one belongs to the endpoint. Without one, MongoDB guarantees nothing: +rows appear on two pages or on none as data changes, and nothing errors. + **`catchError` is the error convention** — use it for all async work rather than `try/catch` around `await`: diff --git a/docs/assets/search-index.js b/docs/assets/search-index.js index 5683574..77735f9 100644 --- a/docs/assets/search-index.js +++ b/docs/assets/search-index.js @@ -1,2 +1,2 @@ /* Generated. Loaded on demand by docs.js when search is first opened. */ -window.PSK_SEARCH_INDEX = [{"t":"parse-server-kit","u":"index.html","s":"","x":""},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html","s":"","x":"This is the part of Parse Server worth building on. Per-row authorisation, enforced by the database layer on every query, is weeks of work in any framework that does not have it — "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#model","s":"Two layers, and both must pass","x":"Parse checks permissions twice, at different granularities. Understanding which layer is refusing you is most of the debugging. CLP — class levelACL — row level QuestionMay this ki"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp","s":"Class-level permissions","x":"Six operations, declared once on the model and written into the schema at boot. import { ParseClass, roleKey, implementACL, cloneAcl, CloudFunction, catchError, BeforeSave, validat"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp-forms","s":"The four forms","x":"Written asMeans {}Master key only. No client reaches it — the strictest setting {requiresAuthentication: true}Any signed-in user {[roleKey(\u0027Editor\u0027)]: true}"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#protected-fields","s":"Hiding individual fields","x":"Sometimes the row should be readable but one column should not. protectedFields removes fields per role, without a second class or a mapping layer. @ParseClass(\u0027Employee\u0027, { clp: {"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#default-acl","s":"The default ACL template — read this one","x":"@ParseClass also writes a default ACL template into the schema. It is applied to objects created without an explicit ACL, and if you do not set it, it is: { \u0027*\u0027: { read: true, writ"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#acl","s":"Row-level ACLs","x":"An ACL is a map from an identity to what it may do. Three kinds of key: KeyIdentity *Everyone, signed in or not role:EditorEvery member of that role, resolved at request time"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#implementacl","s":"implementACL","x":"Takes one params object and returns an ACL. It does not take the object as an argument — a signature worth remembering, because the wrong shape is a common mistake. article.setACL("},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#patterns","s":"Patterns","x":"Most applications need four or five of these. They compose. The record belongs to its creator @CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-owner","s":"The record belongs to its creator","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async createNote(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const note = Note.fromP"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-status","s":"Visibility that follows a status","x":"function articleAcl(authorId: string, status: string) { return implementACL({ owner: [{ user: authorId, read: true, write: true }], roleRules: [{ role: Roles.ADMIN, read: true\u003c"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-team","s":"Scoping to a team or organisation","x":"Parse roles can contain other roles, so a per-tenant role gives you multi-tenancy without a where clause anywhere in the codebase. // One role per organisation: \u0027org:acme\u0027. Members"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-trigger","s":"Enforcing it on every save path","x":"An ACL set in one endpoint protects that endpoint. Setting it in @BeforeSave protects the dashboard, scripts, imports and any endpoint added later. @BeforeSave() static async onBef"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-nested","s":"Nested objects that must follow the parent","x":"A pointed-to record keeps whatever ACL it was created with. Publish the parent and its image stays invisible; hide the parent and its image stays readable. offer.setACL(implementAC"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#master-key","s":"The master key, and why to use it sparingly","x":"{useMasterKey: true} bypasses CLP and ACL completely. It is the right tool for administrative work, migrations and system jobs — and the wrong one for ordinary request handling, be"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#mistakes","s":"Common mistakes","x":"SymptomUsually A record is visible to everyoneNo ACL was ever set, so the class\u0027s default template applied — and unless you declared one, that is public read and write A query retu"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#debugging","s":"Working out which layer refused you","x":"Repeat the call with the master key. If it works, permissions are the cause; if not, look at the query. Read the row\u0027s ACL — obj.getACL()?.toJSON() shows exactly which identities h"},{"t":"AI assistants","u":"guide/ai-assistants.html","s":"","x":"This library\u0027s failure modes are, almost exactly, the mistakes a language model makes by default. That is not a coincidence — and it is fixable by handing your assistant the rules "},{"t":"AI assistants","u":"guide/ai-assistants.html#why","s":"Why this library specifically","x":"A model writing TypeScript reaches for the form that is correct nearly everywhere: @ParseField({ type: \u0027String\u0027, required: true }) title!: string; // correct in every other library"},{"t":"AI assistants","u":"guide/ai-assistants.html#generate","s":"Getting them","x":"psk new asks, before it writes anything: Which AI coding assistants do you use? • 1 Claude Code CLAUDE.md, plus two skills and a review agent 2 Cursor .cursor/rules/parse-server-ki"},{"t":"AI assistants","u":"guide/ai-assistants.html#existing","s":"A project that already exists","x":"Skipped the question, or changed tools since? psk ai writes into the project you are standing in, taking its name from package.json: psk ai # asks, same list psk ai claude cursor #"},{"t":"AI assistants","u":"guide/ai-assistants.html#unattended","s":"Unattended","x":"psk new my-api --ai=claude,agents -y psk new my-api --ai=none -y # nothing written --ai= wins over the prompt, and a non-interactive shell takes the default rather than hanging — s"},{"t":"AI assistants","u":"guide/ai-assistants.html#targets","s":"What each tool gets","x":"Assistant--aiFiles Claude Codeclaude CLAUDE.md.claude/skills/parse-model/SKILL.md.claude/skills/parse-endpoint/SKILL.md.claude/agents/parse-reviewer.md"},{"t":"AI assistants","u":"guide/ai-assistants.html#contents","s":"What is in them","x":"Ten rules, each one a silent failure, each shown with the wrong form next to the right one — a rule stated in the abstract is easy to misapply: RuleWhat happens when it is broken d"},{"t":"AI assistants","u":"guide/ai-assistants.html#index","s":"Knowing what exists","x":"The rules file also carries a one-line index of every export — models, endpoints, all 21 trigger types, cron, permissions, validation, transactions, locking, indexes, schema, middl"},{"t":"AI assistants","u":"guide/ai-assistants.html#skills","s":"Skills and the review agent","x":"Claude Code gets eight more files, because it can load instructions on demand rather than carrying everything in context. That split is deliberate: the rules file is read on every "},{"t":"AI assistants","u":"guide/ai-assistants.html#reference","s":"Pointing an assistant at the full API","x":"The generated files are the rules, not the reference. The complete API — every signature, taken from the source rather than paraphrased — ships inside the package: node_modules/par"},{"t":"AI assistants","u":"guide/ai-assistants.html#honest","s":"What this does not do","x":"Instructions raise the floor; they do not remove the need to read the diff. Two habits catch nearly everything that gets through: Run the build. The decorators are compile-time. A "},{"t":"The psk CLI","u":"guide/cli.html","s":"","x":"Create a project that already runs, and add entities to it without touching a registration file."},{"t":"The psk CLI","u":"guide/cli.html#commands","s":"Commands","x":"CommandDoes psk new \u003cname\u003eCreate a project that boots and serves psk g resource \u003cName\u003eAdd a model and five CRUD endpoints psk g model \u003cName\u003eAdd a model only"},{"t":"The psk CLI","u":"guide/cli.html#new","s":"Creating a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev Fifteen files, and every one of them removes a trap that is otherwise easy to hit: What it setsWh"},{"t":"The psk CLI","u":"guide/cli.html#ai","s":"It asks which assistant you use","x":"Before writing anything, because the same traps in that table are the ones a language model walks into by default — title!: string is correct in every other library and silently br"},{"t":"The psk CLI","u":"guide/cli.html#resource","s":"Adding an entity","x":"psk g resource Product ✓ src/models/Product.ts ✓ src/functions/product.ts Routes now available: POST /api/products/createProduct GET /api/products/listProducts GET /api/products/ge"},{"t":"The psk CLI","u":"guide/cli.html#layout","s":"It follows your layout","x":"The generator looks for where your models already live and writes alongside them, including a per-entity modules/{Name}/functions.ts arrangement. A fresh project gets models/ and f"},{"t":"Parse Dashboard","u":"guide/dashboard.html","s":"","x":"An admin console over every class, query, user and role — the thing you would otherwise build or buy. It is one install away and already wired into the generated project, but it is"},{"t":"Parse Dashboard","u":"guide/dashboard.html#what","s":"What you get","x":"Parse Dashboard is the official web console for a Parse Server. It is a real application, not a table viewer: AreaWhat it does BrowserEvery class as a spreadsheet — read, edit, add"},{"t":"Parse Dashboard","u":"guide/dashboard.html#install","s":"Getting it","x":"Two commands. Nothing to configure. npm install parse-dashboard npm run dev It appears at http://localhost:1337/dashboard. The generated app.ts already looks for it: /** * Mount Pa"},{"t":"Parse Dashboard","u":"guide/dashboard.html#why-not-bundled","s":"Why it is not bundled","x":"Three reasons, in order of how much they matter: It is a large install. The dashboard is a bundled React application with its own dependency tree — a few hundred packages that ever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#security","s":"It holds the master key","x":"This is the whole security story in one sentence The dashboard authenticates to your API with the master key, which bypasses every class-level permission and every row ACL. Whoever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#credentials","s":"Credentials","x":"# .env DASHBOARD_USER=admin DASHBOARD_PASS=something-long-and-unguessable DASHBOARD_PASSDevelopmentProduction set mounts, uses it \u003ctd"},{"t":"Parse Dashboard","u":"guide/dashboard.html#https","s":"HTTPS","x":"Parse Dashboard refuses to serve over plain HTTP unless you pass allowInsecureHTTP, because the master key would be travelling in clear. The template passes it only when NODE_ENV i"},{"t":"Parse Dashboard","u":"guide/dashboard.html#read-only","s":"Read-only operators","x":"Not everyone who needs to look at data needs to change it. Parse Server has a read-only master key, and the dashboard has read-only users to go with it: // ParseServer options read"},{"t":"Parse Dashboard","u":"guide/dashboard.html#middleware","s":"How it sits with the middleware","x":"The dashboard is mounted at /dashboard, outside MOUNT_PATH, so restrictRoutes never sees its requests. That is not a loophole: the dashboard talks to the API with the master key, a"},{"t":"Parse Dashboard","u":"guide/dashboard.html#production","s":"Exposing it in production","x":"In rough order of preference: Do not. Run it locally against the production serverURL when you need it, so there is no permanently reachable console at all. Behind a VPN or an auth"},{"t":"Parse Dashboard","u":"guide/dashboard.html#troubleshooting","s":"When it does not appear","x":"SymptomCause Banner says npm install parse-dashboard Not installed, or installed without restarting Banner says not mounted: set DASHBOARD_PASS NODE_ENV=pr"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html","s":"","x":"The method name is the route. No route table, no controller to keep in sync."},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#route","s":"@Route","x":"import { Route, CloudFunction, catchError, setupSwagger, } from \u0027parse-server-kit\u0027; @Route(Product) // -\u003e /api/products/* @Route(\u0027menu-items\u0027) /"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#cloudfunction","s":"@CloudFunction","x":"@CloudFunction({ methods: [\u0027POST\u0027], description: \u0027Create a product\u0027, validation: { requireUser: true, fields: { name: { required: true } } }, requireRoles: [\u0027Admin\u0027], rateLimit: { "},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#errors","s":"Error handling","x":"const [err, saved] = await catchError(product.save(null, { sessionToken })); if (err) throw err; catchError is the convention throughout the library: it turns a rejected promise in"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#openapi","s":"OpenAPI","x":"Every endpoint appears in the generated document with its real route, derived from @Route — no annotations needed. GET and HEAD emit query parameters; everything else emits a JSON "},{"t":"Errors","u":"guide/errors.html","s":"","x":"What to throw, what the client sees, and which code means what."},{"t":"Errors","u":"guide/errors.html#catcherror","s":"The convention","x":"This library uses catchError rather than try/catch around every await. It turns a rejected promise into a tuple, so the failure is a value you handle rather than a jump. import { c"},{"t":"Errors","u":"guide/errors.html#throwing","s":"Throwing something useful","x":"throw new Parse.Error(142, \u0027Price must be greater than zero\u0027); A Parse.Error reaches the client as {code, error} with a sensible HTTP status. Anything else becomes a generic 141 wi"},{"t":"Errors","u":"guide/errors.html#validation","s":"Validation errors","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // 142, listing every failure at once } validateOrThrow checks e"},{"t":"Errors","u":"guide/errors.html#conflicts","s":"Write conflicts","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Errors","u":"guide/errors.html#responses","s":"What the client receives","x":"removeResultMiddleware unwraps Parse\u0027s {result: …} envelope on success. Errors pass through unchanged: // success { \"objectId\": \"abc123\", \"name\": \"Widget\" } // failure { \u003c"},{"t":"Errors","u":"guide/errors.html#practice","s":"In practice","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async publishArticle(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const [findErr, art"},{"t":"Errors","u":"guide/errors.html#logging","s":"Logging","x":"parse-server logs an uncaught error from a cloud function with the function name, the parameters and the user. Deliberate Parse.Error throws are logged too, so a noisy validation p"},{"t":"Files \u0026 images","u":"guide/files.html","s":"","x":"Uploading, storing and serving files — and the ACL problem that catches everyone the first time."},{"t":"Files \u0026 images","u":"guide/files.html#parsefile","s":"Parse.File","x":"A file is uploaded first and attached second. It has its own lifecycle and its own URL. const file = new Parse.File(\u0027invoice.pdf\u0027, { base64 }, \u0027application/pdf\u0027); await file.save({"},{"t":"Files \u0026 images","u":"guide/files.html#field","s":"Declaring a file field","x":"import { ParseField, implementACL, syncImageAcl, CloudFunction, catchError, BeforeSaveFile, AfterDeleteFile, AfterDelete, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027File\u0027 }) d"},{"t":"Files \u0026 images","u":"guide/files.html#acl","s":"The problem everyone hits","x":"A file\u0027s URL is not protected by the parent record\u0027s ACL. Anyone holding the URL can fetch the bytes. What the ACL protects is the row that points at it. A file URL is effectively "},{"t":"Files \u0026 images","u":"guide/files.html#cascade","s":"Making images follow their parent","x":"When you use a wrapper class, its row has its own ACL — and that ACL does not update itself when the parent\u0027s visibility changes. Publish an article and its cover stays invisible; "},{"t":"Files \u0026 images","u":"guide/files.html#upload-endpoint","s":"An upload endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { base64: { required: true }, fileName: { required: true }, } }, }) static async uploadImage(req: Parse"},{"t":"Files \u0026 images","u":"guide/files.html#triggers","s":"File triggers","x":"@BeforeSaveFile() static async onBeforeSaveFile(req: any) { const { file, user } = req; if (!user) throw new Parse.Error(119, \u0027Sign in to upload\u0027);"},{"t":"Files \u0026 images","u":"guide/files.html#cleanup","s":"Deleting","x":"Deleting a row that points at a file does not delete the file. Storage keeps it until something removes it. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRe"},{"t":"Files \u0026 images","u":"guide/files.html#storage","s":"Where files actually live","x":"AdapterSuits Filesystem (default)Development. Lost on redeploy, not shared between instances S3, Google Cloud StorageProduction GridFSKeeping files in MongoDB — simple, but grows t"},{"t":"Files \u0026 images","u":"guide/files.html#mistakes","s":"Common mistakes","x":"SymptomCause An image is invisible while its record is publicsyncImageAcl not called after the parent\u0027s ACL was set An image stays visible after the record was hiddenSame, on the t"},{"t":"fromParams","u":"guide/from-params.html","s":"","x":"Turns a request body into a typed Parse object, reading your @ParseField declarations to convert pointers, dates and geopoints. It is designed to be pointed straight at raw user in"},{"t":"fromParams","u":"guide/from-params.html#why","s":"The alternative","x":"Without it, every create and update is a run of set() calls that drift from the model each time a field is added: // Twelve lines that must be edited every time the model changes, "},{"t":"fromParams","u":"guide/from-params.html#conversions","s":"What it converts","x":"It walks your declared fields and converts each according to its type. Declared typeSent asBecomes String, Number, Boolean\"Widget\", 12the value, unchanged"},{"t":"fromParams","u":"guide/from-params.html#clearing","s":"Clearing a pointer","x":"Both null and {} mean remove this relationship, so a form that submits an empty select works without special handling: { \"category\": null } // cleared { \"category\": {} } // cleared"},{"t":"fromParams","u":"guide/from-params.html#files","s":"File and image fields are skipped","x":"Pointer fields whose target is an excluded class — IMG and File by default — are deliberately not converted. An uploaded file has to be saved and processed before it can be attache"},{"t":"fromParams","u":"guide/from-params.html#create-update","s":"One call for create and update","x":"// No id in the body -\u003e insert Product.fromParams({ name: \u0027New\u0027 }); // id present -\u003e the same call updates that row Product.fromParams({ id: \u0027prod789\u0027, name: \u0027Edited\u0027 }); That is w"},{"t":"fromParams","u":"guide/from-params.html#safety","s":"Why it is safe on raw input","x":"The obvious worry about mapping a request body onto a model is over-posting: a caller adding {\"isAdmin\": true} and having it stored. That does not happen, and the reason is worth k"},{"t":"fromParams","u":"guide/from-params.html#control","s":"What you do still control","x":"Two things are worth being deliberate about. Neither is a flaw — both are the natural consequence of mapping a body onto a model. Server-controlled fields If a field is declared, a"},{"t":"fromParams","u":"guide/from-params.html#server-fields","s":"Server-controlled fields","x":"If a field is declared, a caller can set it. A status, isApproved or ownerId that your server means to own will be taken from the body if it is there. const product = Product.fromP"},{"t":"fromParams","u":"guide/from-params.html#clp-switch","s":"An id changes which permission applies","x":"Because an id in the body turns the save into an update, Parse checks the update permission rather than create — the row\u0027s ACL write access, and the class\u0027s update CLP. That protec"},{"t":"fromParams","u":"guide/from-params.html#acl-in-body","s":"An ACL sent in the body","x":"A caller can include ACL in the payload. It is not saved — like any undeclared value it stays clean — but getACL() will return it until you set your own. That only matters in one s"},{"t":"fromParams","u":"guide/from-params.html#complete","s":"A complete create endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { name: { required: true } } }, }) static async createProduct(req: Parse.Cloud.FunctionRequest) { const"},{"t":"fromParams","u":"guide/from-params.html#notes","s":"Smaller things","x":"It mutates the params objectSets className on the input so Parse can build from it. Harmless for req.params, surprising if you reuse the same object Absent means untouchedA declare"},{"t":"fromParams","u":"guide/from-params.html#summary","s":"In short","x":"Converts pointers, arrays of pointers, dates and geopoints from your declarations. Never writes a field you did not declare, whatever the caller sends. Skips file and image pointer"},{"t":"Getting started","u":"guide/getting-started.html","s":"","x":"Three commands to a running API with OpenAPI docs, a seeded user, and a MongoDB configured so transactions actually work."},{"t":"Getting started","u":"guide/getting-started.html#requirements","s":"Requirements","x":"Node≥ 20.19 MongoDB≥ 7.0.16 for parse-server 9. A replica set is required for transactions TypeScriptAny version, with experimentalDecorators: t"},{"t":"Getting started","u":"guide/getting-started.html#create","s":"Create a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev The generated project prints a ready-to-paste curl on startup, including a session token for "},{"t":"Getting started","u":"guide/getting-started.html#what-you-get","s":"What was generated","x":"my-api/ ├── docker-compose.yml # MongoDB as a replica set ├── tsconfig.json # decorator flags already set ├── .env └── src/ ├── app.ts # boot order — the sequence matters ├── seed."},{"t":"Getting started","u":"guide/getting-started.html#first-request","s":"Your first request","x":"Both of these are real exchanges against a freshly generated project — the responses below are what the server actually returned. Create — needs the session token printed at startu"},{"t":"Getting started","u":"guide/getting-started.html#dashboard","s":"Parse Dashboard","x":"Parse has an official admin console \u0026mdash; browse and edit every class, run queries, inspect users and roles, send push. It is not installed by default, and getting it is one comm"},{"t":"Getting started","u":"guide/getting-started.html#adding","s":"Adding your own entity","x":"psk g resource Product Writes src/models/Product.ts and src/functions/product.ts with the edit points marked, and changes nothing else — there is no module to register into, becaus"},{"t":"Getting started","u":"guide/getting-started.html#tsconfig","s":"TypeScript setup","x":"If you are adding this to an existing project rather than generating one, one compiler flag is mandatory: { \"compilerOptions\": { \"experimentalDecorators\": true } } This library use"},{"t":"Getting started","u":"guide/getting-started.html#parse-docs","s":"Where Parse\u0027s own docs take over","x":"This site covers what this library adds. For Parse Server itself — query operators, ACL semantics, LiveQuery, file adapters, push, authentication providers — the Parse Platform doc"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html","s":"","x":"Clients subscribe to a query and receive updates as rows change — with the same ACL deciding who sees what."},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#what","s":"What it is","x":"A client sends a query and keeps the connection open. When a row that matches is created, changed or removed, the server pushes the event. No polling, no websocket layer to design,"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#enable","s":"Enabling it","x":"Two settings on Parse Server: which classes may be subscribed to, and the websocket server itself. const parseServer = ParseServer({ // … your usual options … liveQuery: { classNam"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#client","s":"Subscribing, from a client","x":"const query = new Parse.Query(Task); query.equalTo(\u0027project\u0027, Project.pointer(projectId)); query.equalTo(\u0027done\u0027, false); const subscription = await query.subscribe(); subscriptio"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#permissions","s":"Permissions","x":"A subscription carries the client\u0027s session token, and every event is checked against the row\u0027s ACL before it is sent. Two users subscribed to the same query receive different even"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#triggers","s":"Server-side hooks","x":"import { ParseClass, BaseModel, BeforeConnect, BeforeSubscribe, AfterEvent, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Task\u0027) export default class Task extends BaseModel { \u003cspan"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#cost","s":"What it costs","x":"Every save on a live class is matched against every open subscription for it. Keep classNames to the classes that genuinely need it. Connections are stateful — they hold memory and"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#alternatives","s":"When not to use it","x":"NeedBetter Notify a user who is not on the pagePush notification Refresh a dashboard every minutePolling — simpler and cheaper Stream a high-frequency feedA dedicated channel; Live"},{"t":"Models \u0026 fields","u":"guide/models.html","s":"","x":"One class produces the schema, the indexes, the database validators, the permissions and the OpenAPI definition."},{"t":"Models \u0026 fields","u":"guide/models.html#parseclass","s":"@ParseClass","x":"Registers the Parse subclass and everything derived from it. Applied to a class extending BaseModel. import { ParseClass, BaseModel, ParseField, BeforeSave, validateOrThrow, } from"},{"t":"Models \u0026 fields","u":"guide/models.html#parsefield","s":"@ParseField","x":"Defines a getter and setter on the prototype that read and write Parse\u0027s attribute store. That means product.name is the supported API, not product.get(\u0027name\u0027). @ParseField({ type:"},{"t":"Models \u0026 fields","u":"guide/models.html#field-options","s":"Options","x":"OptionApplies toEffect typeallString, Number, Boolean, Date, Object, Array, GeoPoint, File, Bytes, Polygon, Pointer, Relation requiredallEnforced by validateOrThrow and by the data"},{"t":"Models \u0026 fields","u":"guide/models.html#basemodel","s":"BaseModel","x":"// A reference by id, with no fetch. const ref = Category.pointer(\u0027abc123\u0027); // Build a typed instance from request params, reading @ParseField // metadata to convert pointers, arr"},{"t":"Models \u0026 fields","u":"guide/models.html#validation","s":"Validation","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // checks every rule declared above } validateOrThrow checks req"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html","s":"","x":"One line on a model, and a stale save is refused instead of silently overwriting."},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#problem","s":"The problem","x":"Two people open the same record, both save. The second write overwrites the first, and neither is told. Guarding against that in every endpoint means remembering to, every time, fo"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#usage","s":"The whole feature","x":"import { ParseClass, BaseModel, ParseVersionField, createVersionedMongoAdapter, VersionRegistry, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Job\u0027) class Job extends BaseModel {"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#requirements","s":"Requirements","x":"Needs createVersionedMongoAdapter, the same adapter that powers transactions. It is MongoDB only; on Postgres the field is declared but nothing enforces it — and since 3.0 that say"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#handling","s":"Recovering","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { const\u003c/span"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#registry","s":"Inspecting it","x":"VersionRegistry.classNames(); // every versioned class VersionRegistry.isVersioned(\u0027Job\u0027); // true VersionRegistry.adapterIsInstalled(); BackTransactions \u003ca class=\"next\" href=\"seed"},{"t":"Queries","u":"guide/queries.html","s":"","x":"Finding things. Filters, sorting, pagination, pointers, sub-queries and counting — with the permission model doing the filtering you would otherwise write by hand."},{"t":"Queries","u":"guide/queries.html#basics","s":"The shape of a query","x":"Build it, constrain it, run it. Always pass a sessionToken so the ACL applies — see permissions below. import {catchError, ParseClass, CloudFunction} from \u0027parse-server-kit\u0027; const"},{"t":"Queries","u":"guide/queries.html#filters","s":"Filtering","x":"MethodFinds rows where equalTo(k, v)the field equals v notEqualTo(k, v)it does not greaterThan(k, v) / lessThan(k, v)numeric or date comparison"},{"t":"Queries","u":"guide/queries.html#search","s":"Text search","x":"// Case-insensitive substring. Simple, but it cannot use an index - // fine for admin screens, poor for large public listings. query.matches(\u0027name\u0027, new RegExp(search, \u0027i\u0027)); // Pr"},{"t":"Queries","u":"guide/queries.html#combining","s":"Combining conditions","x":"Conditions on one query are combined with AND. For OR, build several queries and join them. const byName = new Parse.Query(Product); byName.matches(\u0027name\u0027, new RegExp(term, \u0027i\u0027)); "},{"t":"Queries","u":"guide/queries.html#pointers","s":"Pointers and related data","x":"A Pointer stores only a reference. Ask for the related row explicitly, or you get {__type, className, objectId} and nothing else. // Fetch the category along with each product quer"},{"t":"Queries","u":"guide/queries.html#sorting","s":"Sorting and pagination","x":"query.descending(\u0027createdAt\u0027); // newest first query.ascending(\u0027name\u0027); // A to Z query.addDescending(\u0027price\u0027); // then by price, within that const\u003c/spa"},{"t":"Queries","u":"guide/queries.html#deep-paging","s":"Paging deeply","x":"skip gets slower the further in you go, because the database still walks the skipped rows. For a large export, page on a sort key instead: let cursor: Date | undefined; while (true"},{"t":"Queries","u":"guide/queries.html#single","s":"Fetching one row","x":"// By id. Throws 101 when missing OR when the ACL hides it. const [err, product] = await catchError(query.get(id, { sessionToken })); // First match, or undefined - no throw const "},{"t":"Queries","u":"guide/queries.html#counting","s":"Counting","x":"const [err, total] = await catchError(query.count({ sessionToken })); // A list endpoint usually wants both, from the same constraints. const rows = await query.find({ sessionToken"},{"t":"Queries","u":"guide/queries.html#permissions","s":"Queries and permissions","x":"This is the part that removes work rather than adding it. // Filtered by the ACL - only rows this user may read come back. await query.find({ sessionToken: user.getSessionToken() }"},{"t":"Queries","u":"guide/queries.html#aggregate","s":"Aggregation and distinct","x":"// Master key required for both. const distinct = await query.distinct(\u0027status\u0027); const pipeline = [ { match: { status: \u0027active\u0027 } }, { group: { objectId: \u0027$category\u0027, total:"},{"t":"Queries","u":"guide/queries.html#patterns","s":"A complete list endpoint","x":"@CloudFunction({ methods: [\u0027GET\u0027], description: \u0027List products\u0027, validation: { fields: { page: { type: String }, limit: { type: String }, status: { type: String }, search: { type: "},{"t":"Queries","u":"guide/queries.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed property access is unavailableThe result was not cast — rows as Product[] A pointer field is {__type: \u0027Pointer\u0027}No include() for it Only 100 rows come backThat"},{"t":"Relationships","u":"guide/relations.html","s":"","x":"Pointer, array of pointers, or Relation — three ways to connect records, and the one question that decides between them."},{"t":"Relationships","u":"guide/relations.html#three","s":"Three options","x":"HoldsGood up toQuery from Pointerone reference—either side Array of pointersa list, stored on the row~100either side Relationa list, stored sep"},{"t":"Relationships","u":"guide/relations.html#pointer","s":"Pointer — one reference","x":"import { ParseField, ParseClass, BaseModel, BeforeSave, cloneAcl, AfterDelete, catchError, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Category\u0027, requir"},{"t":"Relationships","u":"guide/relations.html#one-to-many","s":"One to many","x":"Put the pointer on the many side. A project does not hold a list of tasks; each task points at its project. // Task @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Project\u0027, required:"},{"t":"Relationships","u":"guide/relations.html#array","s":"Array of pointers — a small curated list","x":"// targetClass is what makes fromParams convert the ids @ParseField({ type: \u0027Array\u0027, targetClass: \u0027Tag\u0027 }) declare tags: any[]; { \"tags\":"},{"t":"Relationships","u":"guide/relations.html#relation","s":"Relation — a large list","x":"Stored outside the row, so size does not affect it and adding does not rewrite the parent. @ParseField({ type: \u0027Relation\u0027, targetClass: \u0027User\u0027 }) declare members: any; \u003cspan class="},{"t":"Relationships","u":"guide/relations.html#join","s":"A join model — when the link has its own data","x":"The moment the relationship needs attributes of its own — a role, a joined-at date, an order — it is a record, not a pointer. @ParseClass(\u0027Membership\u0027) export default class Members"},{"t":"Relationships","u":"guide/relations.html#querying","s":"Querying across a relationship","x":"// Tasks whose project is archived - a sub-query const archived = new Parse.Query(Project).equalTo(\u0027status\u0027, \u0027archived\u0027); new Parse.Query(Task).matchesQuery(\u0027project\u0027, archived);"},{"t":"Relationships","u":"guide/relations.html#acl","s":"Permissions across a relationship","x":"A related row has its own ACL. A task is not private because its project is — you have to say so. The usual place is the child\u0027s @BeforeSave: @BeforeSave() static async onBeforeSav"},{"t":"Relationships","u":"guide/relations.html#deleting","s":"Deleting","x":"Parse does not cascade. Deleting a project leaves its tasks pointing at nothing. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRequest\u003cProject\u003e) { const que"},{"t":"Relationships","u":"guide/relations.html#summary","s":"Choosing, briefly","x":"RelationshipUse Task → its projectPointer on the task Project → its tasksNo field. Query tasks by project Post → its tagsArray of pointers Team → its members (many)Relation, or a j"},{"t":"Seeding data","u":"guide/seeding.html","s":"","x":"A fresh install has no roles, so every role-gated endpoint refuses everyone — including you. Seeding is what makes an empty database into a working one, and the generated project s"},{"t":"Seeding data","u":"guide/seeding.html#why","s":"Why an empty database is not a working one","x":"Nothing about this is obvious until it happens. You start the server, everything logs green, and the first request fails: The first POST anyone tries POST /api/notes/createNote X-P"},{"t":"Seeding data","u":"guide/seeding.html#two-kinds","s":"Two kinds of seed, and why they are separate","x":"Almost every seeding mistake comes from treating these as one thing: Reference dataSample data What it isroles, the first admin, currencies, countriesdemo users, example rows Witho"},{"t":"Seeding data","u":"guide/seeding.html#idempotent","s":"Write it so you can run it twice","x":"A seed you are afraid to re-run is a seed that drifts out of step with the code, and you find that out during an incident. Every helper in the template finds first and creates only"},{"t":"Seeding data","u":"guide/seeding.html#roles","s":"Role hierarchy: the direction everyone gets backwards","x":"Making an Admin count as an Editor is one line, and it is the reverse of what reads naturally. Adding role A to role B\u0027s roles relation means every member of A is also treated as a"},{"t":"Seeding data","u":"guide/seeding.html#membership","s":"Adding a user to a role is a save on the role","x":"Role membership is a Parse relation, so the write happens on the role, not the user. This trips people up because setting a role field on the user looks like it should work — and i"},{"t":"Seeding data","u":"guide/seeding.html#running","s":"Two ways to run it","x":"At bootnpm run seed How it reaches the databasein process, directlyover REST, to a running server Server must be upno — it is the serveryes Runs"},{"t":"Seeding data","u":"guide/seeding.html#login-gotcha","s":"Why the standalone seed cannot log in","x":"One real quirk, worth knowing before it puzzles you. The template\u0027s sample seed finishes by logging in as the demo user so the startup banner can print a working session token. Run"},{"t":"Seeding data","u":"guide/seeding.html#output","s":"What a run looks like","x":"$ npm run seed Seed complete. roles created Editor, Admin users created admin, demo notes created 2 And immediately again, unchanged: $ npm run seed Seed complete. roles created no"},{"t":"Seeding data","u":"guide/seeding.html#extending","s":"Adding your own","x":"Seed rows are ordinary model instances, so everything the rest of the documentation says applies — including setting an ACL at creation rather than leaving rows world-readable: for"},{"t":"Seeding data","u":"guide/seeding.html#checklist","s":"Before you ship it","x":"Run it twice. The second run must create nothing. Run it against an empty database. It must succeed from nothing. ADMIN_PASSWORD set outside development. Sample data behind the NOD"},{"t":"Transactions","u":"guide/transactions.html","s":"","x":"Everything the body writes lands together, or none of it does — without threading a session through every call."},{"t":"Transactions","u":"guide/transactions.html#setup","s":"Setup","x":"Transactions are powered by a database adapter. Pass it to Parse Server instead of a plain URI: import { createVersionedMongoAdapter, CloudFunction, Transactional, withTransaction,"},{"t":"Transactions","u":"guide/transactions.html#usage","s":"Using it","x":"@CloudFunction({ methods: [\u0027POST\u0027] }) @Transactional() static async placeOrder(req: Parse.Cloud.FunctionRequest) { await order.save(null, { useMasterKey: true }); \u003cspan class=\""},{"t":"Transactions","u":"guide/transactions.html#semantics","s":"What it guarantees","x":"Nested calls join the outer transaction; the outermost caller commits. The body may re-run — up to three attempts on a transient conflict — so it must be safe to repeat. After thre"},{"t":"Transactions","u":"guide/transactions.html#conflicts","s":"Handling a conflict","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Triggers","u":"guide/triggers.html","s":"","x":"All 21 Parse trigger types as decorators, declared next to the thing they act on."},{"t":"Triggers","u":"guide/triggers.html#usage","s":"Declaring a trigger","x":"import { ParseClass, BaseModel, BeforeSave, Cron, CronSchedule, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Product\u0027) export default class Product extends BaseModel { \u003cspan class="},{"t":"Triggers","u":"guide/triggers.html#types","s":"Every trigger","x":"DecoratorRegisters as @BeforeSave @AfterSavebeforeSave / afterSave @BeforeDelete @AfterDeletebeforeDelete / afterDelete \u003cco"},{"t":"Triggers","u":"guide/triggers.html#cron","s":"Scheduled jobs","x":"class Jobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Nightly cleanup\u0027 }) static async cleanup() { … } } CronSchedule covers the usual patterns from EVERY_MINUT"},{"t":"Troubleshooting","u":"guide/troubleshooting.html","s":"","x":"Symptoms that have no error message attached, and where to look for each."},{"t":"Troubleshooting","u":"guide/troubleshooting.html#index","s":"Symptom index","x":"SymptomCauseWhere Decorators do nothing, or behave oddlyexperimentalDecorators not setGetting started — now throws with a clear message No models, empty schema, no routes"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#fixed","s":"Traps that no longer exist","x":"Two were removed rather than documented, because a warning is not a fix: Fields reading undefined A class field could shadow @ParseField\u0027s accessor at target: ES2022 — reads return"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#checklist","s":"When something will not start","x":"Is experimentalDecorators set? Did importFiles print the files it loaded — and are they the compiled ones? Did the registries initialise after Parse Server was mounted? Read the bo"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html","s":"","x":"A checkout API, where getting it wrong costs real money. Pointers and dates decoded straight from the request body, a total the client cannot forge, stock two shoppers cannot both "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#building","s":"What you are building","x":"A storefront back end with four jobs, in order of how badly they go wrong when done naively: JobThe naive versionWhat you will write Decode the request twenty set() calls that drif"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#setup","s":"1. Set up","x":"npx parse-server-kit new shop-api cd shop-api npm run db:up # MongoDB as a replica set - transactions need one npm run dev The replica set matters. Transactions "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#products","s":"2. Products, with stock worth protecting","x":"psk g resource Product Then fill in src/models/Product.ts: import { ParseClass, ParseField, BaseModel, ParseVersionField, roleKey, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Pr"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#order-model","s":"3. The order model, and every type fromParams decodes","x":"This model exists partly to be realistic and partly to cover, in one place, each field type that fromParams converts for you. @ParseClass(\u0027Order\u0027, { description: \u0027A customer order\u0027"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#placing","s":"4. Placing an order — what the browser actually sends","x":"Here is the whole point of fromParams. The frontend sends flat JSON: ids as strings, dates as strings, a geopoint as a plain object. Your model says what those fields are. fromPara"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#not-fromparams","s":"What fromParams deliberately does not do","x":"It is worth being precise, because the gaps are design decisions rather than omissions: InputResultWhy A key with no @ParseField ignored Only declared fields are set, so an unexpec"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#pricing","s":"5. Never trust the client\u0027s total","x":"The request deliberately did not include a price, and if it had, you would throw it away. Prices come from the database, every time. import { catchError } from \u0027parse-server-kit\u0027; "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#stock","s":"6. Two shoppers, one unit of stock","x":"This is the step that separates a demo from a system. Two requests read stock: 1 at the same moment, both check \"is there enough?\", both say yes, and both write stock: 0. You have "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#conflict","s":"What the loser sees","x":"Two requests, same instant POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"quantity\": 1 }] } POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"q"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#permissions","s":"7. Permissions that follow the order\u0027s status","x":"Now the part that a where clause cannot do. The rule is: the customer may always read their own order; they may edit it only while it is pending; staff may always read and write; n"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#transitions","s":"8. Status transitions, gated by role","x":"A customer must not mark their own order shipped. That is not an ACL question — they legitimately have write access while it is pending — it is a rule about which change is allowed"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#realtime","s":"9. Watching an order in real time","x":"The customer\u0027s page should update itself when the order ships, without polling. Parse\u0027s LiveQuery does that, and — the part that matters — it respects the ACL you already wrote. A "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#whole-flow","s":"10. The whole flow, on the wire","x":"Start to finish, this is every exchange a checkout makes: 1 · Browse GET /api/products/listProducts?limit=20 Public, no token needed { \u003cspan class=\""},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#built","s":"What you built","x":"ProblemWhat handles itLines you wrote Decoding pointers, dates, geopoints, id arraysOrder.fromParams()1 Forged totalsserver-side pricinga helper Overselling under load"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html","s":"","x":"A small task-tracking API, end to end: models, relationships, permissions that scope data per user, search, and a scheduled job. About twenty minutes."},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#setup","s":"1. Start the project","x":"npx parse-server-kit new tasks-api cd tasks-api npm run db:up npm run dev You now have a running server with a Note example, docs at /api-docs, and a seeded demo"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#model","s":"2. The first model","x":"Delete the example and generate your own: psk g resource Project Open src/models/Project.ts and fill in the fields: import { ParseClass, BaseModel, ParseField, BeforeSave, validate"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#ownership","s":"3. Make projects private to their owner","x":"The CLP above lets any signed-in user reach the class. The ACL decides which rows they see. Set it when the project is created — in src/functions/project.ts: static async createPro"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#relationship","s":"4. A second model, related to the first","x":"psk g resource Task @ParseField({ type: \u0027String\u0027, required: true }) declare title: string; @ParseField({ type: \u0027Boolean\u0027 }) \u003cspan class="},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#inherit-acl","s":"Tasks should follow their project","x":"A task is as private as the project it belongs to. Do that in the trigger, so it holds on every save path: @BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveReques"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#listing","s":"5. A list endpoint that filters and paginates","x":"@CloudFunction({ methods: [\u0027GET\u0027], validation: { fields: { project: { type: String }, done: { type: String }, search: { type: String }, page: { type: String }, } }, }) static async"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#cron","s":"6. A scheduled job","x":"Create src/cron.ts and import it from app.ts before the registries initialise: class TaskJobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Flag overdue tasks\u0027 }) "},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#try","s":"7. Try it","x":"export APP=tasks-api export TOK=\u003cthe token printed at startup\u003e curl -X POST localhost:1337/api/projects/createProject \\ -H \"X-Parse-Application-Id: $APP\" -H \"X-Parse-Session-Token:"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#what-next","s":"What you built","x":"Two models with a relationshipschema, indexes and validators derived from them Ten REST endpointsrouted from method names, documented automatically Per-row permissionsenforced by t"},{"t":"Users \u0026 authentication","u":"guide/users.html","s":"","x":"Signup, login, sessions, roles and password reset — the part you would otherwise spend a fortnight building."},{"t":"Users \u0026 authentication","u":"guide/users.html#why","s":"What you get without writing it","x":"Parse ships a _User class, hashed passwords, session tokens with revocation, email verification, password reset, and a _Role system that the ACL layer already understands. None of "},{"t":"Users \u0026 authentication","u":"guide/users.html#model","s":"Extending the user","x":"Add your own fields by declaring a model against the built-in class. import { ParseClass, ParseField, CloudFunction, catchError, invalidateRoles, getUserRoles, getUsersRoles, Befor"},{"t":"Users \u0026 authentication","u":"guide/users.html#signup","s":"Signing up","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { fields: { username: { required: true }, password: { required: true }, email: { required: true }, } }, }) static async signUp(req: "},{"t":"Users \u0026 authentication","u":"guide/users.html#login","s":"Logging in","x":"const [err, user] = await catchError( Parse.User.logIn(req.params.username, req.params.password) ); if (err) throw err; // 101 for wrong username or password return { sessionToken:"},{"t":"Users \u0026 authentication","u":"guide/users.html#sessions","s":"Sessions","x":"A session token identifies the caller on every later request. Clients send it as a header: X-Parse-Application-Id: my-api X-Parse-Session-Token: r:8f2c… Inside a cloud function it "},{"t":"Users \u0026 authentication","u":"guide/users.html#roles","s":"Roles","x":"A role is a named set of users, and it is what an ACL and a CLP refer to. // Create once, at setup or in a seed. const acl = new Parse.ACL(); acl.setPublicReadAccess(true); // so A"},{"t":"Users \u0026 authentication","u":"guide/users.html#password-reset","s":"Password reset and email","x":"await Parse.User.requestPasswordReset(email); Parse sends the mail, hosts the reset page and expires the link. It needs an email adapter and a publicServerURL configured on the ser"},{"t":"Users \u0026 authentication","u":"guide/users.html#protecting","s":"Protecting user data","x":"_User rows are readable by default in many setups. Two things to decide deliberately: @ParseClass(\u0027_User\u0027, { clp: { find: { requiresAuthentication: true }, get: { requiresAuthentic"},{"t":"Users \u0026 authentication","u":"guide/users.html#hooks","s":"Reacting to auth events","x":"@BeforeLogin() static async onBeforeLogin(req: any) { if (req.object.get(\u0027suspended\u0027)) { throw new Parse.Error(101, \u0027Account suspended\u0027); } } \u003cs"},{"t":"Users \u0026 authentication","u":"guide/users.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed fields on req.user are unavailableNot cast — req.user! as User A user can edit another userSaved with useMasterKey instead of the caller\u0027s session token Password"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html","s":"","x":"They are not really competitors. One gives you structure and you build the backend; the other is a backend you customise. Here is the honest side-by-side."},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#difference","s":"The actual difference","x":"NestJS is an application framework. It gives you dependency injection, module boundaries and a request lifecycle — and you build authentication, users, roles, permissions, real-tim"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#table","s":"Feature by feature","x":"NestJSparse-server-kit Auth, sessions, password resetyou build itbuilt in Users \u0026 rolesyou build itbuilt in Row"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#side-by-side","s":"The same feature, both ways","x":"\"Users may only see their own orders\" The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable("},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-permissions","s":"\"Users may only see their own orders\"","x":"The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable() export class OrdersService { constru"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-auth","s":"Signup and login","x":"// NestJS: install passport, jwt, bcrypt; write a User entity, an // AuthService, a JwtStrategy, guards, refresh tokens, a password // reset flow with tokens and expiry, and an ema"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-crud","s":"A CRUD resource","x":"// NestJS: nest g resource orders // orders.module.ts, orders.controller.ts, orders.service.ts, // dto/create-order.dto.ts, dto/update-order.dto.ts, // entities/order.entity.ts + r"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose","s":"When to choose NestJS","x":"Genuinely — these are good reasons, not concessions. You need a database Parse does not serve well. Postgres works, but transactions and optimistic locking here are MongoDB-only. Y"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose-parse","s":"When to choose this","x":"The app is mostly records with permissions. Business apps, admin tools, marketplaces, mobile backends — most software, honestly. Per-row authorisation is a real requirement. Multi-"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#honest","s":"The honest summary","x":"A fair way to decide Write down what your backend must do. Cross out everything Parse already provides — auth, users, roles, row-level permissions, real-time, files, push, dashboar"},{"t":"Configuration","u":"reference/configuration.html","s":"","x":"Settings, schema generation, indexes and the optional role cache."},{"t":"Configuration","u":"reference/configuration.html#configurekit","s":"configureKit","x":"Optional. Every default reproduces what the library did before these became settings, so a project that never calls it is unaffected. Values resolve when they are used, not at impo"},{"t":"Configuration","u":"reference/configuration.html#allowauthroutes","s":"allowAuthRoutes","x":"restrictRoutes closes Parse\u0027s generic REST API so clients go through the cloud functions you declared. Its auth endpoints are closed along with everything else, and the documented "},{"t":"Configuration","u":"reference/configuration.html#schema","s":"createSchemaConfig","x":"const schema = createSchemaConfig({ adminRole: \u0027Owner\u0027 }); // pass as the `schema` option to ParseServer OptionDefaultEffect adminRolefrom"},{"t":"Configuration","u":"reference/configuration.html#indexes","s":"Indexes","x":"await applyAllIndexes(parseServer); // after listen() await applyMongoValidators(parseServer); Declared asCreates @ParseField({unique: true})"},{"t":"Configuration","u":"reference/configuration.html#role-cache","s":"Role cache","x":"Off by default. requireRoles and getUserRoles() each cost a database round-trip. Role membership rarely changes, so it caches well — but a revoked role keeps working until the entr"},{"t":"Decorators","u":"reference/decorators.html","s":"","x":"Every decorator the package exports, with its signature."},{"t":"Decorators","u":"reference/decorators.html#model","s":"Model","x":"DecoratorTargetPurpose @ParseClass(name, options?)classRegisters the subclass, CLP, ACL template, compound indexes; registers with OpenAPI and flushes pending triggers @ParseField("},{"t":"Decorators","u":"reference/decorators.html#endpoints","s":"Endpoints","x":"@CloudFunction(config)static methodRegisters a cloud function with HTTP methods, validation and roles @ProtectedCloudFunction(config?)static methodAs above, with POST and requireUs"},{"t":"Decorators","u":"reference/decorators.html#triggers","s":"Triggers","x":"All take an optional {description?, validation?}. Object@BeforeSave @AfterSave @BeforeDelete @AfterDelete @BeforeFind @AfterFind Auth@BeforeLogin @AfterLogin @AfterL"},{"t":"Decorators","u":"reference/decorators.html#registries","s":"Registries","x":"Each is initialised once at boot, after Parse Server is mounted. CloudFunctionRegistryinitialize() also builds the route map and reports the versioning setup TriggerRegistryinitial"},{"t":"Decorators","u":"reference/decorators.html#hooks","s":"Registration hooks","x":"For integrations that need to observe registration as it happens. import { onClassRegistered, onFieldRegistered, onFunctionRegistered, } from \u0027parse-server-kit\u0027; onClassRegistered("},{"t":"Middleware","u":"reference/middleware.html","s":"","x":"What each middleware does, and the order they belong in."},{"t":"Middleware","u":"reference/middleware.html#order","s":"Boot order","x":"The sequence is load-bearing. Each step depends on the one before it. import { importFiles, CloudFunctionRegistry, TriggerRegistry, CronRegistry, setupSwagger, } from \u0027parse-server"},{"t":"Middleware","u":"reference/middleware.html#what","s":"What each one does","x":"MiddlewareResponsibility removeResultMiddlewareUnwraps Parse\u0027s {result: …} envelope so responses look like an ordinary REST API validateEntityRoutesResolves /api/{entity}/{action} "},{"t":"Middleware","u":"reference/middleware.html#security","s":"Security notes","x":"The master key is read from the X-Parse-Master-Key header. The request body is still accepted for compatibility and warns once — a secret in a body ends up in every log that record"},{"t":"Utilities","u":"reference/utilities.html","s":"","x":"Helpers, constants and types the package exports."},{"t":"Utilities","u":"reference/utilities.html#catcherror","s":"catchError","x":"The error convention throughout the library — a rejected promise becomes a tuple rather than a try/catch around every await. import { catchError, importFiles, getUserRoles, getUser"},{"t":"Utilities","u":"reference/utilities.html#importfiles","s":"importFiles","x":"importFiles(dir); // .js only importFiles(dir, { extensions: [\u0027.js\u0027, \u0027.ts\u0027] }); // ts-node / tsx importFiles(dir, { verbose: false }); Defaults to .js, the compile"},{"t":"Utilities","u":"reference/utilities.html#roles","s":"Roles","x":"await getUserRoles(user); // string[] await getUsersRoles(users); // Map roleKey(\u0027Editor\u0027); // \u0027role:Editor\u0027, typed as that literal getUsersRole"},{"t":"Utilities","u":"reference/utilities.html#misc","s":"Everything else","x":"syncImageAcl(parent, fields, acl?)Copies a parent\u0027s ACL onto nested image pointers cloneAcl(acl)Deep copy of a Parse.ACL validateObject(object){valid, errors[]} against the declare"},{"t":"Utilities","u":"reference/utilities.html#deprecated","s":"Deprecated","x":"Still exported so existing imports resolve; to be removed in the next major. UserRolesTwo example role names from one project. Declare your own enum and pass it to roleKey, which a"},{"u":"guide/api-docs.html","t":"API documentation","s":"What you get","x":"setupSwagger serves two things. The spec itself is always available at json; the browser UI needs swagger-ui-express, which the generated project installs for you. Call it after the registries have run, or the spec is generated before the endpoints exist and comes back nearly empty."},{"u":"guide/api-docs.html","t":"API documentation","s":"Where the content comes from","x":"Schemas from ParseClass and ParseField. Paths from Route plus the method name. Parameters or request body from validation fields. Security from requiresAuth, requireRoles, requireUser. Summaries, tags and responses from the swagger block on CloudFunction."},{"u":"guide/api-docs.html","t":"API documentation","s":"Tell the page your application id","x":"Every request needs X-Parse-Application-Id and a docs page that does not say which value is a guessing game. A wrong guess comes back as a bare unauthorized that names nothing. Pass appId to setupSwagger and the Authorize dialog shows it. The application id is not a secret."},{"u":"guide/api-docs.html","t":"API documentation","s":"The REST API key is opt-in","x":"Parse only enforces X-Parse-REST-API-Key when restAPIKey is configured, which most deployments do not do. The spec does not require it by default. Pass restApiKey true when you have one."},{"u":"guide/api-docs.html","t":"API documentation","s":"Without swagger-ui-express","x":"The UI package is optional. Without it setupSwagger logs a warning, skips the browser page and still serves the spec at api-docs json. If api-docs returns Cannot GET while api-docs json works, the package is simply not installed."},{"u":"guide/api-docs.html","t":"API documentation","s":"Every option","x":"setupSwagger takes app, config and an optional path that defaults to api-docs. Options are title, version, description, basePath, appId, restApiKey, host and schemes. The spec is always served at the path plus json."},{"u":"guide/api-docs.html","t":"API documentation","s":"Turning it on and off","x":"There is deliberately no enabled option. The way to not have documentation is to not mount it. The generated project reads DOCS_ENABLED and DOCS_PATH from env, so switching docs off is an environment variable rather than a code change, and the banner reports off instead of advertising a dead URL. Docs are on in production by default."}]; +window.PSK_SEARCH_INDEX = [{"t":"parse-server-kit","u":"index.html","s":"","x":""},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html","s":"","x":"This is the part of Parse Server worth building on. Per-row authorisation, enforced by the database layer on every query, is weeks of work in any framework that does not have it — "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#model","s":"Two layers, and both must pass","x":"Parse checks permissions twice, at different granularities. Understanding which layer is refusing you is most of the debugging. CLP — class levelACL — row level QuestionMay this ki"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp","s":"Class-level permissions","x":"Six operations, declared once on the model and written into the schema at boot. import { ParseClass, roleKey, implementACL, cloneAcl, CloudFunction, catchError, BeforeSave, validat"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp-forms","s":"The four forms","x":"Written asMeans {}Master key only. No client reaches it — the strictest setting {requiresAuthentication: true}Any signed-in user {[roleKey(\u0027Editor\u0027)]: true}"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#protected-fields","s":"Hiding individual fields","x":"Sometimes the row should be readable but one column should not. protectedFields removes fields per role, without a second class or a mapping layer. @ParseClass(\u0027Employee\u0027, { clp: {"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#default-acl","s":"The default ACL template — read this one","x":"@ParseClass also writes a default ACL template into the schema. It is applied to objects created without an explicit ACL, and if you do not set it, it is: { \u0027*\u0027: { read: true, writ"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#acl","s":"Row-level ACLs","x":"An ACL is a map from an identity to what it may do. Three kinds of key: KeyIdentity *Everyone, signed in or not role:EditorEvery member of that role, resolved at request time"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#implementacl","s":"implementACL","x":"Takes one params object and returns an ACL. It does not take the object as an argument — a signature worth remembering, because the wrong shape is a common mistake. article.setACL("},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#patterns","s":"Patterns","x":"Most applications need four or five of these. They compose. The record belongs to its creator @CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-owner","s":"The record belongs to its creator","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async createNote(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const note = Note.fromP"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-status","s":"Visibility that follows a status","x":"function articleAcl(authorId: string, status: string) { return implementACL({ owner: [{ user: authorId, read: true, write: true }], roleRules: [{ role: Roles.ADMIN, read: true\u003c"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-team","s":"Scoping to a team or organisation","x":"Parse roles can contain other roles, so a per-tenant role gives you multi-tenancy without a where clause anywhere in the codebase. // One role per organisation: \u0027org:acme\u0027. Members"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-trigger","s":"Enforcing it on every save path","x":"An ACL set in one endpoint protects that endpoint. Setting it in @BeforeSave protects the dashboard, scripts, imports and any endpoint added later. @BeforeSave() static async onBef"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-nested","s":"Nested objects that must follow the parent","x":"A pointed-to record keeps whatever ACL it was created with. Publish the parent and its image stays invisible; hide the parent and its image stays readable. offer.setACL(implementAC"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#master-key","s":"The master key, and why to use it sparingly","x":"{useMasterKey: true} bypasses CLP and ACL completely. It is the right tool for administrative work, migrations and system jobs — and the wrong one for ordinary request handling, be"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#mistakes","s":"Common mistakes","x":"SymptomUsually A record is visible to everyoneNo ACL was ever set, so the class\u0027s default template applied — and unless you declared one, that is public read and write A query retu"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#debugging","s":"Working out which layer refused you","x":"Repeat the call with the master key. If it works, permissions are the cause; if not, look at the query. Read the row\u0027s ACL — obj.getACL()?.toJSON() shows exactly which identities h"},{"t":"AI assistants","u":"guide/ai-assistants.html","s":"","x":"This library\u0027s failure modes are, almost exactly, the mistakes a language model makes by default. That is not a coincidence — and it is fixable by handing your assistant the rules "},{"t":"AI assistants","u":"guide/ai-assistants.html#why","s":"Why this library specifically","x":"A model writing TypeScript reaches for the form that is correct nearly everywhere: @ParseField({ type: \u0027String\u0027, required: true }) title!: string; // correct in every other library"},{"t":"AI assistants","u":"guide/ai-assistants.html#generate","s":"Getting them","x":"psk new asks, before it writes anything: Which AI coding assistants do you use? • 1 Claude Code CLAUDE.md, plus two skills and a review agent 2 Cursor .cursor/rules/parse-server-ki"},{"t":"AI assistants","u":"guide/ai-assistants.html#existing","s":"A project that already exists","x":"Skipped the question, or changed tools since? psk ai writes into the project you are standing in, taking its name from package.json: psk ai # asks, same list psk ai claude cursor #"},{"t":"AI assistants","u":"guide/ai-assistants.html#unattended","s":"Unattended","x":"psk new my-api --ai=claude,agents -y psk new my-api --ai=none -y # nothing written --ai= wins over the prompt, and a non-interactive shell takes the default rather than hanging — s"},{"t":"AI assistants","u":"guide/ai-assistants.html#targets","s":"What each tool gets","x":"Assistant--aiFiles Claude Codeclaude CLAUDE.md.claude/skills/parse-model/SKILL.md.claude/skills/parse-endpoint/SKILL.md.claude/agents/parse-reviewer.md"},{"t":"AI assistants","u":"guide/ai-assistants.html#contents","s":"What is in them","x":"Ten rules, each one a silent failure, each shown with the wrong form next to the right one — a rule stated in the abstract is easy to misapply: RuleWhat happens when it is broken d"},{"t":"AI assistants","u":"guide/ai-assistants.html#index","s":"Knowing what exists","x":"The rules file also carries a one-line index of every export — models, endpoints, all 21 trigger types, cron, permissions, validation, transactions, locking, indexes, schema, middl"},{"t":"AI assistants","u":"guide/ai-assistants.html#skills","s":"Skills and the review agent","x":"Claude Code gets eight more files, because it can load instructions on demand rather than carrying everything in context. That split is deliberate: the rules file is read on every "},{"t":"AI assistants","u":"guide/ai-assistants.html#reference","s":"Pointing an assistant at the full API","x":"The generated files are the rules, not the reference. The complete API — every signature, taken from the source rather than paraphrased — ships inside the package: node_modules/par"},{"t":"AI assistants","u":"guide/ai-assistants.html#honest","s":"What this does not do","x":"Instructions raise the floor; they do not remove the need to read the diff. Two habits catch nearly everything that gets through: Run the build. The decorators are compile-time. A "},{"t":"The psk CLI","u":"guide/cli.html","s":"","x":"Create a project that already runs, and add entities to it without touching a registration file."},{"t":"The psk CLI","u":"guide/cli.html#commands","s":"Commands","x":"CommandDoes psk new \u003cname\u003eCreate a project that boots and serves psk g resource \u003cName\u003eAdd a model and five CRUD endpoints psk g model \u003cName\u003eAdd a model only"},{"t":"The psk CLI","u":"guide/cli.html#new","s":"Creating a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev Fifteen files, and every one of them removes a trap that is otherwise easy to hit: What it setsWh"},{"t":"The psk CLI","u":"guide/cli.html#ai","s":"It asks which assistant you use","x":"Before writing anything, because the same traps in that table are the ones a language model walks into by default — title!: string is correct in every other library and silently br"},{"t":"The psk CLI","u":"guide/cli.html#resource","s":"Adding an entity","x":"psk g resource Product ✓ src/models/Product.ts ✓ src/functions/product.ts Routes now available: POST /api/products/createProduct GET /api/products/listProducts GET /api/products/ge"},{"t":"The psk CLI","u":"guide/cli.html#layout","s":"It follows your layout","x":"The generator looks for where your models already live and writes alongside them, including a per-entity modules/{Name}/functions.ts arrangement. A fresh project gets models/ and f"},{"t":"Parse Dashboard","u":"guide/dashboard.html","s":"","x":"An admin console over every class, query, user and role — the thing you would otherwise build or buy. It is one install away and already wired into the generated project, but it is"},{"t":"Parse Dashboard","u":"guide/dashboard.html#what","s":"What you get","x":"Parse Dashboard is the official web console for a Parse Server. It is a real application, not a table viewer: AreaWhat it does BrowserEvery class as a spreadsheet — read, edit, add"},{"t":"Parse Dashboard","u":"guide/dashboard.html#install","s":"Getting it","x":"Two commands. Nothing to configure. npm install parse-dashboard npm run dev It appears at http://localhost:1337/dashboard. The generated app.ts already looks for it: /** * Mount Pa"},{"t":"Parse Dashboard","u":"guide/dashboard.html#why-not-bundled","s":"Why it is not bundled","x":"Three reasons, in order of how much they matter: It is a large install. The dashboard is a bundled React application with its own dependency tree — a few hundred packages that ever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#security","s":"It holds the master key","x":"This is the whole security story in one sentence The dashboard authenticates to your API with the master key, which bypasses every class-level permission and every row ACL. Whoever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#credentials","s":"Credentials","x":"# .env DASHBOARD_USER=admin DASHBOARD_PASS=something-long-and-unguessable DASHBOARD_PASSDevelopmentProduction set mounts, uses it \u003ctd"},{"t":"Parse Dashboard","u":"guide/dashboard.html#https","s":"HTTPS","x":"Parse Dashboard refuses to serve over plain HTTP unless you pass allowInsecureHTTP, because the master key would be travelling in clear. The template passes it only when NODE_ENV i"},{"t":"Parse Dashboard","u":"guide/dashboard.html#read-only","s":"Read-only operators","x":"Not everyone who needs to look at data needs to change it. Parse Server has a read-only master key, and the dashboard has read-only users to go with it: // ParseServer options read"},{"t":"Parse Dashboard","u":"guide/dashboard.html#middleware","s":"How it sits with the middleware","x":"The dashboard is mounted at /dashboard, outside MOUNT_PATH, so restrictRoutes never sees its requests. That is not a loophole: the dashboard talks to the API with the master key, a"},{"t":"Parse Dashboard","u":"guide/dashboard.html#production","s":"Exposing it in production","x":"In rough order of preference: Do not. Run it locally against the production serverURL when you need it, so there is no permanently reachable console at all. Behind a VPN or an auth"},{"t":"Parse Dashboard","u":"guide/dashboard.html#troubleshooting","s":"When it does not appear","x":"SymptomCause Banner says npm install parse-dashboard Not installed, or installed without restarting Banner says not mounted: set DASHBOARD_PASS NODE_ENV=pr"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html","s":"","x":"The method name is the route. No route table, no controller to keep in sync."},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#route","s":"@Route","x":"import { Route, CloudFunction, catchError, setupSwagger, } from \u0027parse-server-kit\u0027; @Route(Product) // -\u003e /api/products/* @Route(\u0027menu-items\u0027) /"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#cloudfunction","s":"@CloudFunction","x":"@CloudFunction({ methods: [\u0027POST\u0027], description: \u0027Create a product\u0027, validation: { requireUser: true, fields: { name: { required: true } } }, requireRoles: [\u0027Admin\u0027], rateLimit: { "},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#errors","s":"Error handling","x":"const [err, saved] = await catchError(product.save(null, { sessionToken })); if (err) throw err; catchError is the convention throughout the library: it turns a rejected promise in"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#openapi","s":"OpenAPI","x":"Every endpoint appears in the generated document with its real route, derived from @Route — no annotations needed. GET and HEAD emit query parameters; everything else emits a JSON "},{"t":"Errors","u":"guide/errors.html","s":"","x":"What to throw, what the client sees, and which code means what."},{"t":"Errors","u":"guide/errors.html#catcherror","s":"The convention","x":"This library uses catchError rather than try/catch around every await. It turns a rejected promise into a tuple, so the failure is a value you handle rather than a jump. import { c"},{"t":"Errors","u":"guide/errors.html#throwing","s":"Throwing something useful","x":"throw new Parse.Error(142, \u0027Price must be greater than zero\u0027); A Parse.Error reaches the client as {code, error} with a sensible HTTP status. Anything else becomes a generic 141 wi"},{"t":"Errors","u":"guide/errors.html#validation","s":"Validation errors","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // 142, listing every failure at once } validateOrThrow checks e"},{"t":"Errors","u":"guide/errors.html#conflicts","s":"Write conflicts","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Errors","u":"guide/errors.html#responses","s":"What the client receives","x":"removeResultMiddleware unwraps Parse\u0027s {result: …} envelope on success. Errors pass through unchanged: // success { \"objectId\": \"abc123\", \"name\": \"Widget\" } // failure { \u003c"},{"t":"Errors","u":"guide/errors.html#practice","s":"In practice","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async publishArticle(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const [findErr, art"},{"t":"Errors","u":"guide/errors.html#logging","s":"Logging","x":"parse-server logs an uncaught error from a cloud function with the function name, the parameters and the user. Deliberate Parse.Error throws are logged too, so a noisy validation p"},{"t":"Files \u0026 images","u":"guide/files.html","s":"","x":"Uploading, storing and serving files — and the ACL problem that catches everyone the first time."},{"t":"Files \u0026 images","u":"guide/files.html#parsefile","s":"Parse.File","x":"A file is uploaded first and attached second. It has its own lifecycle and its own URL. const file = new Parse.File(\u0027invoice.pdf\u0027, { base64 }, \u0027application/pdf\u0027); await file.save({"},{"t":"Files \u0026 images","u":"guide/files.html#field","s":"Declaring a file field","x":"import { ParseField, implementACL, syncImageAcl, CloudFunction, catchError, BeforeSaveFile, AfterDeleteFile, AfterDelete, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027File\u0027 }) d"},{"t":"Files \u0026 images","u":"guide/files.html#acl","s":"The problem everyone hits","x":"A file\u0027s URL is not protected by the parent record\u0027s ACL. Anyone holding the URL can fetch the bytes. What the ACL protects is the row that points at it. A file URL is effectively "},{"t":"Files \u0026 images","u":"guide/files.html#cascade","s":"Making images follow their parent","x":"When you use a wrapper class, its row has its own ACL — and that ACL does not update itself when the parent\u0027s visibility changes. Publish an article and its cover stays invisible; "},{"t":"Files \u0026 images","u":"guide/files.html#upload-endpoint","s":"An upload endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { base64: { required: true }, fileName: { required: true }, } }, }) static async uploadImage(req: Parse"},{"t":"Files \u0026 images","u":"guide/files.html#triggers","s":"File triggers","x":"@BeforeSaveFile() static async onBeforeSaveFile(req: any) { const { file, user } = req; if (!user) throw new Parse.Error(119, \u0027Sign in to upload\u0027);"},{"t":"Files \u0026 images","u":"guide/files.html#cleanup","s":"Deleting","x":"Deleting a row that points at a file does not delete the file. Storage keeps it until something removes it. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRe"},{"t":"Files \u0026 images","u":"guide/files.html#storage","s":"Where files actually live","x":"AdapterSuits Filesystem (default)Development. Lost on redeploy, not shared between instances S3, Google Cloud StorageProduction GridFSKeeping files in MongoDB — simple, but grows t"},{"t":"Files \u0026 images","u":"guide/files.html#mistakes","s":"Common mistakes","x":"SymptomCause An image is invisible while its record is publicsyncImageAcl not called after the parent\u0027s ACL was set An image stays visible after the record was hiddenSame, on the t"},{"t":"fromParams","u":"guide/from-params.html","s":"","x":"Turns a request body into a typed Parse object, reading your @ParseField declarations to convert pointers, dates and geopoints. It is designed to be pointed straight at raw user in"},{"t":"fromParams","u":"guide/from-params.html#why","s":"The alternative","x":"Without it, every create and update is a run of set() calls that drift from the model each time a field is added: // Twelve lines that must be edited every time the model changes, "},{"t":"fromParams","u":"guide/from-params.html#conversions","s":"What it converts","x":"It walks your declared fields and converts each according to its type. Declared typeSent asBecomes String, Number, Boolean\"Widget\", 12the value, unchanged"},{"t":"fromParams","u":"guide/from-params.html#clearing","s":"Clearing a pointer","x":"Both null and {} mean remove this relationship, so a form that submits an empty select works without special handling: { \"category\": null } // cleared { \"category\": {} } // cleared"},{"t":"fromParams","u":"guide/from-params.html#files","s":"File and image fields are skipped","x":"Pointer fields whose target is an excluded class — IMG and File by default — are deliberately not converted. An uploaded file has to be saved and processed before it can be attache"},{"t":"fromParams","u":"guide/from-params.html#create-update","s":"One call for create and update","x":"// No id in the body -\u003e insert Product.fromParams({ name: \u0027New\u0027 }); // id present -\u003e the same call updates that row Product.fromParams({ id: \u0027prod789\u0027, name: \u0027Edited\u0027 }); That is w"},{"t":"fromParams","u":"guide/from-params.html#safety","s":"Why it is safe on raw input","x":"The obvious worry about mapping a request body onto a model is over-posting: a caller adding {\"isAdmin\": true} and having it stored. That does not happen, and the reason is worth k"},{"t":"fromParams","u":"guide/from-params.html#control","s":"What you do still control","x":"Two things are worth being deliberate about. Neither is a flaw — both are the natural consequence of mapping a body onto a model. Server-controlled fields If a field is declared, a"},{"t":"fromParams","u":"guide/from-params.html#server-fields","s":"Server-controlled fields","x":"If a field is declared, a caller can set it. A status, isApproved or ownerId that your server means to own will be taken from the body if it is there. const product = Product.fromP"},{"t":"fromParams","u":"guide/from-params.html#clp-switch","s":"An id changes which permission applies","x":"Because an id in the body turns the save into an update, Parse checks the update permission rather than create — the row\u0027s ACL write access, and the class\u0027s update CLP. That protec"},{"t":"fromParams","u":"guide/from-params.html#acl-in-body","s":"An ACL sent in the body","x":"A caller can include ACL in the payload. It is not saved — like any undeclared value it stays clean — but getACL() will return it until you set your own. That only matters in one s"},{"t":"fromParams","u":"guide/from-params.html#complete","s":"A complete create endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { name: { required: true } } }, }) static async createProduct(req: Parse.Cloud.FunctionRequest) { const"},{"t":"fromParams","u":"guide/from-params.html#notes","s":"Smaller things","x":"It mutates the params objectSets className on the input so Parse can build from it. Harmless for req.params, surprising if you reuse the same object Absent means untouchedA declare"},{"t":"fromParams","u":"guide/from-params.html#summary","s":"In short","x":"Converts pointers, arrays of pointers, dates and geopoints from your declarations. Never writes a field you did not declare, whatever the caller sends. Skips file and image pointer"},{"t":"Getting started","u":"guide/getting-started.html","s":"","x":"Three commands to a running API with OpenAPI docs, a seeded user, and a MongoDB configured so transactions actually work."},{"t":"Getting started","u":"guide/getting-started.html#requirements","s":"Requirements","x":"Node≥ 20.19 MongoDB≥ 7.0.16 for parse-server 9. A replica set is required for transactions TypeScriptAny version, with experimentalDecorators: t"},{"t":"Getting started","u":"guide/getting-started.html#create","s":"Create a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev The generated project prints a ready-to-paste curl on startup, including a session token for "},{"t":"Getting started","u":"guide/getting-started.html#what-you-get","s":"What was generated","x":"my-api/ ├── docker-compose.yml # MongoDB as a replica set ├── tsconfig.json # decorator flags already set ├── .env └── src/ ├── app.ts # boot order — the sequence matters ├── seed."},{"t":"Getting started","u":"guide/getting-started.html#first-request","s":"Your first request","x":"Both of these are real exchanges against a freshly generated project — the responses below are what the server actually returned. Create — needs the session token printed at startu"},{"t":"Getting started","u":"guide/getting-started.html#dashboard","s":"Parse Dashboard","x":"Parse has an official admin console \u0026mdash; browse and edit every class, run queries, inspect users and roles, send push. It is not installed by default, and getting it is one comm"},{"t":"Getting started","u":"guide/getting-started.html#adding","s":"Adding your own entity","x":"psk g resource Product Writes src/models/Product.ts and src/functions/product.ts with the edit points marked, and changes nothing else — there is no module to register into, becaus"},{"t":"Getting started","u":"guide/getting-started.html#tsconfig","s":"TypeScript setup","x":"If you are adding this to an existing project rather than generating one, one compiler flag is mandatory: { \"compilerOptions\": { \"experimentalDecorators\": true } } This library use"},{"t":"Getting started","u":"guide/getting-started.html#parse-docs","s":"Where Parse\u0027s own docs take over","x":"This site covers what this library adds. For Parse Server itself — query operators, ACL semantics, LiveQuery, file adapters, push, authentication providers — the Parse Platform doc"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html","s":"","x":"Clients subscribe to a query and receive updates as rows change — with the same ACL deciding who sees what."},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#what","s":"What it is","x":"A client sends a query and keeps the connection open. When a row that matches is created, changed or removed, the server pushes the event. No polling, no websocket layer to design,"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#enable","s":"Enabling it","x":"Two settings on Parse Server: which classes may be subscribed to, and the websocket server itself. const parseServer = ParseServer({ // … your usual options … liveQuery: { classNam"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#client","s":"Subscribing, from a client","x":"const query = new Parse.Query(Task); query.equalTo(\u0027project\u0027, Project.pointer(projectId)); query.equalTo(\u0027done\u0027, false); const subscription = await query.subscribe(); subscriptio"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#permissions","s":"Permissions","x":"A subscription carries the client\u0027s session token, and every event is checked against the row\u0027s ACL before it is sent. Two users subscribed to the same query receive different even"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#triggers","s":"Server-side hooks","x":"import { ParseClass, BaseModel, BeforeConnect, BeforeSubscribe, AfterEvent, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Task\u0027) export default class Task extends BaseModel { \u003cspan"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#cost","s":"What it costs","x":"Every save on a live class is matched against every open subscription for it. Keep classNames to the classes that genuinely need it. Connections are stateful — they hold memory and"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#alternatives","s":"When not to use it","x":"NeedBetter Notify a user who is not on the pagePush notification Refresh a dashboard every minutePolling — simpler and cheaper Stream a high-frequency feedA dedicated channel; Live"},{"t":"Models \u0026 fields","u":"guide/models.html","s":"","x":"One class produces the schema, the indexes, the database validators, the permissions and the OpenAPI definition."},{"t":"Models \u0026 fields","u":"guide/models.html#parseclass","s":"@ParseClass","x":"Registers the Parse subclass and everything derived from it. Applied to a class extending BaseModel. import { ParseClass, BaseModel, ParseField, BeforeSave, validateOrThrow, } from"},{"t":"Models \u0026 fields","u":"guide/models.html#parsefield","s":"@ParseField","x":"Defines a getter and setter on the prototype that read and write Parse\u0027s attribute store. That means product.name is the supported API, not product.get(\u0027name\u0027). @ParseField({ type:"},{"t":"Models \u0026 fields","u":"guide/models.html#field-options","s":"Options","x":"OptionApplies toEffect typeallString, Number, Boolean, Date, Object, Array, GeoPoint, File, Bytes, Polygon, Pointer, Relation requiredallEnforced by validateOrThrow and by the data"},{"t":"Models \u0026 fields","u":"guide/models.html#basemodel","s":"BaseModel","x":"// A reference by id, with no fetch. const ref = Category.pointer(\u0027abc123\u0027); // Build a typed instance from request params, reading @ParseField // metadata to convert pointers, arr"},{"t":"Models \u0026 fields","u":"guide/models.html#validation","s":"Validation","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // checks every rule declared above } validateOrThrow checks req"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html","s":"","x":"One line on a model, and a stale save is refused instead of silently overwriting."},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#problem","s":"The problem","x":"Two people open the same record, both save. The second write overwrites the first, and neither is told. Guarding against that in every endpoint means remembering to, every time, fo"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#usage","s":"The whole feature","x":"import { ParseClass, BaseModel, ParseVersionField, createVersionedMongoAdapter, VersionRegistry, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Job\u0027) class Job extends BaseModel {"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#requirements","s":"Requirements","x":"Needs createVersionedMongoAdapter, the same adapter that powers transactions. It is MongoDB only; on Postgres the field is declared but nothing enforces it — and since 3.0 that say"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#handling","s":"Recovering","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { const\u003c/span"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#registry","s":"Inspecting it","x":"VersionRegistry.classNames(); // every versioned class VersionRegistry.isVersioned(\u0027Job\u0027); // true VersionRegistry.adapterIsInstalled(); BackTransactions \u003ca class=\"next\" href=\"seed"},{"t":"Queries","u":"guide/queries.html","s":"","x":"Finding things. Filters, sorting, pagination, pointers, sub-queries and counting — with the permission model doing the filtering you would otherwise write by hand."},{"t":"Queries","u":"guide/queries.html#basics","s":"The shape of a query","x":"Build it, constrain it, run it. Always pass a sessionToken so the ACL applies — see permissions below. import {catchError, ParseClass, CloudFunction} from \u0027parse-server-kit\u0027; const"},{"t":"Queries","u":"guide/queries.html#filters","s":"Filtering","x":"MethodFinds rows where equalTo(k, v)the field equals v notEqualTo(k, v)it does not greaterThan(k, v) / lessThan(k, v)numeric or date comparison"},{"t":"Queries","u":"guide/queries.html#search","s":"Text search","x":"// Case-insensitive substring. Simple, but it cannot use an index - // fine for admin screens, poor for large public listings. query.matches(\u0027name\u0027, new RegExp(search, \u0027i\u0027)); // Pr"},{"t":"Queries","u":"guide/queries.html#combining","s":"Combining conditions","x":"Conditions on one query are combined with AND. For OR, build several queries and join them. const byName = new Parse.Query(Product); byName.matches(\u0027name\u0027, new RegExp(term, \u0027i\u0027)); "},{"t":"Queries","u":"guide/queries.html#pointers","s":"Pointers and related data","x":"A Pointer stores only a reference. Ask for the related row explicitly, or you get {__type, className, objectId} and nothing else. // Fetch the category along with each product quer"},{"t":"Queries","u":"guide/queries.html#sorting","s":"Sorting and pagination","x":"query.descending(\u0027createdAt\u0027); // newest first query.ascending(\u0027name\u0027); // A to Z query.addDescending(\u0027price\u0027); // then by price, within that const\u003c/spa"},{"t":"Queries","u":"guide/queries.html#deep-paging","s":"Paging deeply","x":"skip gets slower the further in you go, because the database still walks the skipped rows. For a large export, page on a sort key instead: let cursor: Date | undefined; while (true"},{"t":"Queries","u":"guide/queries.html#single","s":"Fetching one row","x":"// By id. Throws 101 when missing OR when the ACL hides it. const [err, product] = await catchError(query.get(id, { sessionToken })); // First match, or undefined - no throw const "},{"t":"Queries","u":"guide/queries.html#counting","s":"Counting","x":"const [err, total] = await catchError(query.count({ sessionToken })); // A list endpoint usually wants both, from the same constraints. const rows = await query.find({ sessionToken"},{"t":"Queries","u":"guide/queries.html#permissions","s":"Queries and permissions","x":"This is the part that removes work rather than adding it. // Filtered by the ACL - only rows this user may read come back. await query.find({ sessionToken: user.getSessionToken() }"},{"t":"Queries","u":"guide/queries.html#aggregate","s":"Aggregation and distinct","x":"// Master key required for both. const distinct = await query.distinct(\u0027status\u0027); const pipeline = [ { match: { status: \u0027active\u0027 } }, { group: { objectId: \u0027$category\u0027, total:"},{"t":"Queries","u":"guide/queries.html#patterns","s":"A complete list endpoint","x":"@CloudFunction({ methods: [\u0027GET\u0027], description: \u0027List products\u0027, validation: { fields: { page: { type: String }, limit: { type: String }, status: { type: String }, search: { type: "},{"t":"Queries","u":"guide/queries.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed property access is unavailableThe result was not cast — rows as Product[] A pointer field is {__type: \u0027Pointer\u0027}No include() for it Only 100 rows come backThat"},{"t":"Relationships","u":"guide/relations.html","s":"","x":"Pointer, array of pointers, or Relation — three ways to connect records, and the one question that decides between them."},{"t":"Relationships","u":"guide/relations.html#three","s":"Three options","x":"HoldsGood up toQuery from Pointerone reference—either side Array of pointersa list, stored on the row~100either side Relationa list, stored sep"},{"t":"Relationships","u":"guide/relations.html#pointer","s":"Pointer — one reference","x":"import { ParseField, ParseClass, BaseModel, BeforeSave, cloneAcl, AfterDelete, catchError, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Category\u0027, requir"},{"t":"Relationships","u":"guide/relations.html#one-to-many","s":"One to many","x":"Put the pointer on the many side. A project does not hold a list of tasks; each task points at its project. // Task @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Project\u0027, required:"},{"t":"Relationships","u":"guide/relations.html#array","s":"Array of pointers — a small curated list","x":"// targetClass is what makes fromParams convert the ids @ParseField({ type: \u0027Array\u0027, targetClass: \u0027Tag\u0027 }) declare tags: any[]; { \"tags\":"},{"t":"Relationships","u":"guide/relations.html#relation","s":"Relation — a large list","x":"Stored outside the row, so size does not affect it and adding does not rewrite the parent. @ParseField({ type: \u0027Relation\u0027, targetClass: \u0027User\u0027 }) declare members: any; \u003cspan class="},{"t":"Relationships","u":"guide/relations.html#join","s":"A join model — when the link has its own data","x":"The moment the relationship needs attributes of its own — a role, a joined-at date, an order — it is a record, not a pointer. @ParseClass(\u0027Membership\u0027) export default class Members"},{"t":"Relationships","u":"guide/relations.html#querying","s":"Querying across a relationship","x":"// Tasks whose project is archived - a sub-query const archived = new Parse.Query(Project).equalTo(\u0027status\u0027, \u0027archived\u0027); new Parse.Query(Task).matchesQuery(\u0027project\u0027, archived);"},{"t":"Relationships","u":"guide/relations.html#acl","s":"Permissions across a relationship","x":"A related row has its own ACL. A task is not private because its project is — you have to say so. The usual place is the child\u0027s @BeforeSave: @BeforeSave() static async onBeforeSav"},{"t":"Relationships","u":"guide/relations.html#deleting","s":"Deleting","x":"Parse does not cascade. Deleting a project leaves its tasks pointing at nothing. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRequest\u003cProject\u003e) { const que"},{"t":"Relationships","u":"guide/relations.html#summary","s":"Choosing, briefly","x":"RelationshipUse Task → its projectPointer on the task Project → its tasksNo field. Query tasks by project Post → its tagsArray of pointers Team → its members (many)Relation, or a j"},{"t":"Seeding data","u":"guide/seeding.html","s":"","x":"A fresh install has no roles, so every role-gated endpoint refuses everyone — including you. Seeding is what makes an empty database into a working one, and the generated project s"},{"t":"Seeding data","u":"guide/seeding.html#why","s":"Why an empty database is not a working one","x":"Nothing about this is obvious until it happens. You start the server, everything logs green, and the first request fails: The first POST anyone tries POST /api/notes/createNote X-P"},{"t":"Seeding data","u":"guide/seeding.html#two-kinds","s":"Two kinds of seed, and why they are separate","x":"Almost every seeding mistake comes from treating these as one thing: Reference dataSample data What it isroles, the first admin, currencies, countriesdemo users, example rows Witho"},{"t":"Seeding data","u":"guide/seeding.html#idempotent","s":"Write it so you can run it twice","x":"A seed you are afraid to re-run is a seed that drifts out of step with the code, and you find that out during an incident. Every helper in the template finds first and creates only"},{"t":"Seeding data","u":"guide/seeding.html#roles","s":"Role hierarchy: the direction everyone gets backwards","x":"Making an Admin count as an Editor is one line, and it is the reverse of what reads naturally. Adding role A to role B\u0027s roles relation means every member of A is also treated as a"},{"t":"Seeding data","u":"guide/seeding.html#membership","s":"Adding a user to a role is a save on the role","x":"Role membership is a Parse relation, so the write happens on the role, not the user. This trips people up because setting a role field on the user looks like it should work — and i"},{"t":"Seeding data","u":"guide/seeding.html#running","s":"Two ways to run it","x":"At bootnpm run seed How it reaches the databasein process, directlyover REST, to a running server Server must be upno — it is the serveryes Runs"},{"t":"Seeding data","u":"guide/seeding.html#login-gotcha","s":"Why the standalone seed cannot log in","x":"One real quirk, worth knowing before it puzzles you. The template\u0027s sample seed finishes by logging in as the demo user so the startup banner can print a working session token. Run"},{"t":"Seeding data","u":"guide/seeding.html#output","s":"What a run looks like","x":"$ npm run seed Seed complete. roles created Editor, Admin users created admin, demo notes created 2 And immediately again, unchanged: $ npm run seed Seed complete. roles created no"},{"t":"Seeding data","u":"guide/seeding.html#extending","s":"Adding your own","x":"Seed rows are ordinary model instances, so everything the rest of the documentation says applies — including setting an ACL at creation rather than leaving rows world-readable: for"},{"t":"Seeding data","u":"guide/seeding.html#checklist","s":"Before you ship it","x":"Run it twice. The second run must create nothing. Run it against an empty database. It must succeed from nothing. ADMIN_PASSWORD set outside development. Sample data behind the NOD"},{"t":"Transactions","u":"guide/transactions.html","s":"","x":"Everything the body writes lands together, or none of it does — without threading a session through every call."},{"t":"Transactions","u":"guide/transactions.html#setup","s":"Setup","x":"Transactions are powered by a database adapter. Pass it to Parse Server instead of a plain URI: import { createVersionedMongoAdapter, CloudFunction, Transactional, withTransaction,"},{"t":"Transactions","u":"guide/transactions.html#usage","s":"Using it","x":"@CloudFunction({ methods: [\u0027POST\u0027] }) @Transactional() static async placeOrder(req: Parse.Cloud.FunctionRequest) { await order.save(null, { useMasterKey: true }); \u003cspan class=\""},{"t":"Transactions","u":"guide/transactions.html#semantics","s":"What it guarantees","x":"Nested calls join the outer transaction; the outermost caller commits. The body may re-run — up to three attempts on a transient conflict — so it must be safe to repeat. After thre"},{"t":"Transactions","u":"guide/transactions.html#conflicts","s":"Handling a conflict","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Triggers","u":"guide/triggers.html","s":"","x":"All 21 Parse trigger types as decorators, declared next to the thing they act on."},{"t":"Triggers","u":"guide/triggers.html#usage","s":"Declaring a trigger","x":"import { ParseClass, BaseModel, BeforeSave, Cron, CronSchedule, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Product\u0027) export default class Product extends BaseModel { \u003cspan class="},{"t":"Triggers","u":"guide/triggers.html#types","s":"Every trigger","x":"DecoratorRegisters as @BeforeSave @AfterSavebeforeSave / afterSave @BeforeDelete @AfterDeletebeforeDelete / afterDelete \u003cco"},{"t":"Triggers","u":"guide/triggers.html#cron","s":"Scheduled jobs","x":"class Jobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Nightly cleanup\u0027 }) static async cleanup() { … } } CronSchedule covers the usual patterns from EVERY_MINUT"},{"t":"Troubleshooting","u":"guide/troubleshooting.html","s":"","x":"Symptoms that have no error message attached, and where to look for each."},{"t":"Troubleshooting","u":"guide/troubleshooting.html#index","s":"Symptom index","x":"SymptomCauseWhere Decorators do nothing, or behave oddlyexperimentalDecorators not setGetting started — now throws with a clear message No models, empty schema, no routes"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#fixed","s":"Traps that no longer exist","x":"Two were removed rather than documented, because a warning is not a fix: Fields reading undefined A class field could shadow @ParseField\u0027s accessor at target: ES2022 — reads return"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#checklist","s":"When something will not start","x":"Is experimentalDecorators set? Did importFiles print the files it loaded — and are they the compiled ones? Did the registries initialise after Parse Server was mounted? Read the bo"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html","s":"","x":"A checkout API, where getting it wrong costs real money. Pointers and dates decoded straight from the request body, a total the client cannot forge, stock two shoppers cannot both "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#building","s":"What you are building","x":"A storefront back end with four jobs, in order of how badly they go wrong when done naively: JobThe naive versionWhat you will write Decode the request twenty set() calls that drif"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#setup","s":"1. Set up","x":"npx parse-server-kit new shop-api cd shop-api npm run db:up # MongoDB as a replica set - transactions need one npm run dev The replica set matters. Transactions "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#products","s":"2. Products, with stock worth protecting","x":"psk g resource Product Then fill in src/models/Product.ts: import { ParseClass, ParseField, BaseModel, ParseVersionField, roleKey, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Pr"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#order-model","s":"3. The order model, and every type fromParams decodes","x":"This model exists partly to be realistic and partly to cover, in one place, each field type that fromParams converts for you. @ParseClass(\u0027Order\u0027, { description: \u0027A customer order\u0027"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#placing","s":"4. Placing an order — what the browser actually sends","x":"Here is the whole point of fromParams. The frontend sends flat JSON: ids as strings, dates as strings, a geopoint as a plain object. Your model says what those fields are. fromPara"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#not-fromparams","s":"What fromParams deliberately does not do","x":"It is worth being precise, because the gaps are design decisions rather than omissions: InputResultWhy A key with no @ParseField ignored Only declared fields are set, so an unexpec"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#pricing","s":"5. Never trust the client\u0027s total","x":"The request deliberately did not include a price, and if it had, you would throw it away. Prices come from the database, every time. import { catchError } from \u0027parse-server-kit\u0027; "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#stock","s":"6. Two shoppers, one unit of stock","x":"This is the step that separates a demo from a system. Two requests read stock: 1 at the same moment, both check \"is there enough?\", both say yes, and both write stock: 0. You have "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#conflict","s":"What the loser sees","x":"Two requests, same instant POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"quantity\": 1 }] } POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"q"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#permissions","s":"7. Permissions that follow the order\u0027s status","x":"Now the part that a where clause cannot do. The rule is: the customer may always read their own order; they may edit it only while it is pending; staff may always read and write; n"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#transitions","s":"8. Status transitions, gated by role","x":"A customer must not mark their own order shipped. That is not an ACL question — they legitimately have write access while it is pending — it is a rule about which change is allowed"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#realtime","s":"9. Watching an order in real time","x":"The customer\u0027s page should update itself when the order ships, without polling. Parse\u0027s LiveQuery does that, and — the part that matters — it respects the ACL you already wrote. A "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#whole-flow","s":"10. The whole flow, on the wire","x":"Start to finish, this is every exchange a checkout makes: 1 · Browse GET /api/products/listProducts?limit=20 Public, no token needed { \u003cspan class=\""},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#built","s":"What you built","x":"ProblemWhat handles itLines you wrote Decoding pointers, dates, geopoints, id arraysOrder.fromParams()1 Forged totalsserver-side pricinga helper Overselling under load"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html","s":"","x":"A small task-tracking API, end to end: models, relationships, permissions that scope data per user, search, and a scheduled job. About twenty minutes."},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#setup","s":"1. Start the project","x":"npx parse-server-kit new tasks-api cd tasks-api npm run db:up npm run dev You now have a running server with a Note example, docs at /api-docs, and a seeded demo"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#model","s":"2. The first model","x":"Delete the example and generate your own: psk g resource Project Open src/models/Project.ts and fill in the fields: import { ParseClass, BaseModel, ParseField, BeforeSave, validate"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#ownership","s":"3. Make projects private to their owner","x":"The CLP above lets any signed-in user reach the class. The ACL decides which rows they see. Set it when the project is created — in src/functions/project.ts: static async createPro"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#relationship","s":"4. A second model, related to the first","x":"psk g resource Task @ParseField({ type: \u0027String\u0027, required: true }) declare title: string; @ParseField({ type: \u0027Boolean\u0027 }) \u003cspan class="},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#inherit-acl","s":"Tasks should follow their project","x":"A task is as private as the project it belongs to. Do that in the trigger, so it holds on every save path: @BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveReques"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#listing","s":"5. A list endpoint that filters and paginates","x":"@CloudFunction({ methods: [\u0027GET\u0027], validation: { fields: { project: { type: String }, done: { type: String }, search: { type: String }, page: { type: String }, } }, }) static async"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#cron","s":"6. A scheduled job","x":"Create src/cron.ts and import it from app.ts before the registries initialise: class TaskJobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Flag overdue tasks\u0027 }) "},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#try","s":"7. Try it","x":"export APP=tasks-api export TOK=\u003cthe token printed at startup\u003e curl -X POST localhost:1337/api/projects/createProject \\ -H \"X-Parse-Application-Id: $APP\" -H \"X-Parse-Session-Token:"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#what-next","s":"What you built","x":"Two models with a relationshipschema, indexes and validators derived from them Ten REST endpointsrouted from method names, documented automatically Per-row permissionsenforced by t"},{"t":"Users \u0026 authentication","u":"guide/users.html","s":"","x":"Signup, login, sessions, roles and password reset — the part you would otherwise spend a fortnight building."},{"t":"Users \u0026 authentication","u":"guide/users.html#why","s":"What you get without writing it","x":"Parse ships a _User class, hashed passwords, session tokens with revocation, email verification, password reset, and a _Role system that the ACL layer already understands. None of "},{"t":"Users \u0026 authentication","u":"guide/users.html#model","s":"Extending the user","x":"Add your own fields by declaring a model against the built-in class. import { ParseClass, ParseField, CloudFunction, catchError, invalidateRoles, getUserRoles, getUsersRoles, Befor"},{"t":"Users \u0026 authentication","u":"guide/users.html#signup","s":"Signing up","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { fields: { username: { required: true }, password: { required: true }, email: { required: true }, } }, }) static async signUp(req: "},{"t":"Users \u0026 authentication","u":"guide/users.html#login","s":"Logging in","x":"const [err, user] = await catchError( Parse.User.logIn(req.params.username, req.params.password) ); if (err) throw err; // 101 for wrong username or password return { sessionToken:"},{"t":"Users \u0026 authentication","u":"guide/users.html#sessions","s":"Sessions","x":"A session token identifies the caller on every later request. Clients send it as a header: X-Parse-Application-Id: my-api X-Parse-Session-Token: r:8f2c… Inside a cloud function it "},{"t":"Users \u0026 authentication","u":"guide/users.html#roles","s":"Roles","x":"A role is a named set of users, and it is what an ACL and a CLP refer to. // Create once, at setup or in a seed. const acl = new Parse.ACL(); acl.setPublicReadAccess(true); // so A"},{"t":"Users \u0026 authentication","u":"guide/users.html#password-reset","s":"Password reset and email","x":"await Parse.User.requestPasswordReset(email); Parse sends the mail, hosts the reset page and expires the link. It needs an email adapter and a publicServerURL configured on the ser"},{"t":"Users \u0026 authentication","u":"guide/users.html#protecting","s":"Protecting user data","x":"_User rows are readable by default in many setups. Two things to decide deliberately: @ParseClass(\u0027_User\u0027, { clp: { find: { requiresAuthentication: true }, get: { requiresAuthentic"},{"t":"Users \u0026 authentication","u":"guide/users.html#hooks","s":"Reacting to auth events","x":"@BeforeLogin() static async onBeforeLogin(req: any) { if (req.object.get(\u0027suspended\u0027)) { throw new Parse.Error(101, \u0027Account suspended\u0027); } } \u003cs"},{"t":"Users \u0026 authentication","u":"guide/users.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed fields on req.user are unavailableNot cast — req.user! as User A user can edit another userSaved with useMasterKey instead of the caller\u0027s session token Password"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html","s":"","x":"They are not really competitors. One gives you structure and you build the backend; the other is a backend you customise. Here is the honest side-by-side."},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#difference","s":"The actual difference","x":"NestJS is an application framework. It gives you dependency injection, module boundaries and a request lifecycle — and you build authentication, users, roles, permissions, real-tim"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#table","s":"Feature by feature","x":"NestJSparse-server-kit Auth, sessions, password resetyou build itbuilt in Users \u0026 rolesyou build itbuilt in Row"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#side-by-side","s":"The same feature, both ways","x":"\"Users may only see their own orders\" The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable("},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-permissions","s":"\"Users may only see their own orders\"","x":"The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable() export class OrdersService { constru"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-auth","s":"Signup and login","x":"// NestJS: install passport, jwt, bcrypt; write a User entity, an // AuthService, a JwtStrategy, guards, refresh tokens, a password // reset flow with tokens and expiry, and an ema"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-crud","s":"A CRUD resource","x":"// NestJS: nest g resource orders // orders.module.ts, orders.controller.ts, orders.service.ts, // dto/create-order.dto.ts, dto/update-order.dto.ts, // entities/order.entity.ts + r"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose","s":"When to choose NestJS","x":"Genuinely — these are good reasons, not concessions. You need a database Parse does not serve well. Postgres works, but transactions and optimistic locking here are MongoDB-only. Y"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose-parse","s":"When to choose this","x":"The app is mostly records with permissions. Business apps, admin tools, marketplaces, mobile backends — most software, honestly. Per-row authorisation is a real requirement. Multi-"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#honest","s":"The honest summary","x":"A fair way to decide Write down what your backend must do. Cross out everything Parse already provides — auth, users, roles, row-level permissions, real-time, files, push, dashboar"},{"t":"Configuration","u":"reference/configuration.html","s":"","x":"Settings, schema generation, indexes and the optional role cache."},{"t":"Configuration","u":"reference/configuration.html#configurekit","s":"configureKit","x":"Optional. Every default reproduces what the library did before these became settings, so a project that never calls it is unaffected. Values resolve when they are used, not at impo"},{"t":"Configuration","u":"reference/configuration.html#allowauthroutes","s":"allowAuthRoutes","x":"restrictRoutes closes Parse\u0027s generic REST API so clients go through the cloud functions you declared. Its auth endpoints are closed along with everything else, and the documented "},{"t":"Configuration","u":"reference/configuration.html#schema","s":"createSchemaConfig","x":"const schema = createSchemaConfig({ adminRole: \u0027Owner\u0027 }); // pass as the `schema` option to ParseServer OptionDefaultEffect adminRolefrom"},{"t":"Configuration","u":"reference/configuration.html#indexes","s":"Indexes","x":"await applyAllIndexes(parseServer); // after listen() await applyMongoValidators(parseServer); Declared asCreates @ParseField({unique: true})"},{"t":"Configuration","u":"reference/configuration.html#role-cache","s":"Role cache","x":"Off by default. requireRoles and getUserRoles() each cost a database round-trip. Role membership rarely changes, so it caches well — but a revoked role keeps working until the entr"},{"t":"Decorators","u":"reference/decorators.html","s":"","x":"Every decorator the package exports, with its signature."},{"t":"Decorators","u":"reference/decorators.html#model","s":"Model","x":"DecoratorTargetPurpose @ParseClass(name, options?)classRegisters the subclass, CLP, ACL template, compound indexes; registers with OpenAPI and flushes pending triggers @ParseField("},{"t":"Decorators","u":"reference/decorators.html#endpoints","s":"Endpoints","x":"@CloudFunction(config)static methodRegisters a cloud function with HTTP methods, validation and roles @ProtectedCloudFunction(config?)static methodAs above, with POST and requireUs"},{"t":"Decorators","u":"reference/decorators.html#triggers","s":"Triggers","x":"All take an optional {description?, validation?}. Object@BeforeSave @AfterSave @BeforeDelete @AfterDelete @BeforeFind @AfterFind Auth@BeforeLogin @AfterLogin @AfterL"},{"t":"Decorators","u":"reference/decorators.html#registries","s":"Registries","x":"Each is initialised once at boot, after Parse Server is mounted. CloudFunctionRegistryinitialize() also builds the route map and reports the versioning setup TriggerRegistryinitial"},{"t":"Decorators","u":"reference/decorators.html#hooks","s":"Registration hooks","x":"For integrations that need to observe registration as it happens. import { onClassRegistered, onFieldRegistered, onFunctionRegistered, } from \u0027parse-server-kit\u0027; onClassRegistered("},{"t":"Middleware","u":"reference/middleware.html","s":"","x":"What each middleware does, and the order they belong in."},{"t":"Middleware","u":"reference/middleware.html#order","s":"Boot order","x":"The sequence is load-bearing. Each step depends on the one before it. import { importFiles, CloudFunctionRegistry, TriggerRegistry, CronRegistry, setupSwagger, } from \u0027parse-server"},{"t":"Middleware","u":"reference/middleware.html#what","s":"What each one does","x":"MiddlewareResponsibility removeResultMiddlewareUnwraps Parse\u0027s {result: …} envelope so responses look like an ordinary REST API validateEntityRoutesResolves /api/{entity}/{action} "},{"t":"Middleware","u":"reference/middleware.html#security","s":"Security notes","x":"The master key is read from the X-Parse-Master-Key header. The request body is still accepted for compatibility and warns once — a secret in a body ends up in every log that record"},{"t":"Utilities","u":"reference/utilities.html","s":"","x":"Helpers, constants and types the package exports."},{"t":"Utilities","u":"reference/utilities.html#catcherror","s":"catchError","x":"The error convention throughout the library — a rejected promise becomes a tuple rather than a try/catch around every await. import { catchError, importFiles, getUserRoles, getUser"},{"t":"Utilities","u":"reference/utilities.html#importfiles","s":"importFiles","x":"importFiles(dir); // .js only importFiles(dir, { extensions: [\u0027.js\u0027, \u0027.ts\u0027] }); // ts-node / tsx importFiles(dir, { verbose: false }); Defaults to .js, the compile"},{"t":"Utilities","u":"reference/utilities.html#roles","s":"Roles","x":"await getUserRoles(user); // string[] await getUsersRoles(users); // Map roleKey(\u0027Editor\u0027); // \u0027role:Editor\u0027, typed as that literal getUsersRole"},{"t":"Utilities","u":"reference/utilities.html#misc","s":"Everything else","x":"syncImageAcl(parent, fields, acl?)Copies a parent\u0027s ACL onto nested image pointers cloneAcl(acl)Deep copy of a Parse.ACL validateObject(object){valid, errors[]} against the declare"},{"t":"Utilities","u":"reference/utilities.html#deprecated","s":"Deprecated","x":"Still exported so existing imports resolve; to be removed in the next major. UserRolesTwo example role names from one project. Declare your own enum and pass it to roleKey, which a"},{"u":"guide/api-docs.html","t":"API documentation","s":"What you get","x":"setupSwagger serves two things. The spec itself is always available at json; the browser UI needs swagger-ui-express, which the generated project installs for you. Call it after the registries have run, or the spec is generated before the endpoints exist and comes back nearly empty."},{"u":"guide/api-docs.html","t":"API documentation","s":"Where the content comes from","x":"Schemas from ParseClass and ParseField. Paths from Route plus the method name. Parameters or request body from validation fields. Security from requiresAuth, requireRoles, requireUser. Summaries, tags and responses from the swagger block on CloudFunction."},{"u":"guide/api-docs.html","t":"API documentation","s":"Tell the page your application id","x":"Every request needs X-Parse-Application-Id and a docs page that does not say which value is a guessing game. A wrong guess comes back as a bare unauthorized that names nothing. Pass appId to setupSwagger and the Authorize dialog shows it. The application id is not a secret."},{"u":"guide/api-docs.html","t":"API documentation","s":"The REST API key is opt-in","x":"Parse only enforces X-Parse-REST-API-Key when restAPIKey is configured, which most deployments do not do. The spec does not require it by default. Pass restApiKey true when you have one."},{"u":"guide/api-docs.html","t":"API documentation","s":"Without swagger-ui-express","x":"The UI package is optional. Without it setupSwagger logs a warning, skips the browser page and still serves the spec at api-docs json. If api-docs returns Cannot GET while api-docs json works, the package is simply not installed."},{"u":"guide/api-docs.html","t":"API documentation","s":"Every option","x":"setupSwagger takes app, config and an optional path that defaults to api-docs. Options are title, version, description, basePath, appId, restApiKey, host and schemes. The spec is always served at the path plus json."},{"u":"guide/api-docs.html","t":"API documentation","s":"Turning it on and off","x":"There is deliberately no enabled option. The way to not have documentation is to not mount it. The generated project reads DOCS_ENABLED and DOCS_PATH from env, so switching docs off is an environment variable rather than a code change, and the banner reports off instead of advertising a dead URL. Docs are on in production by default."},{"u":"guide/queries.html","t":"Queries","s":"Or use paginate","x":"paginate reads limit and skip as the strings a GET sends, caps the limit, and asks the database for the total matching rows in the same round trip via withCount. Returns results, count, limit, skip and hasMore. The hand-written version returns results.length which is the size of the page you already have and no client can paginate with it."},{"u":"guide/from-params.html","t":"fromParams","s":"Server-controlled fields","x":"If a field is declared a caller can set it. That is mass assignment. Declare clientWritable false on the model and fromParams discards the field whatever the request contains. It governs fromParams only, not the field, so your own code writes it freely. The mirror of protectedFields which hides fields on the way out."}]; diff --git a/docs/assets/search-index.json b/docs/assets/search-index.json index 5aa0eb5..617d465 100644 --- a/docs/assets/search-index.json +++ b/docs/assets/search-index.json @@ -1 +1 @@ -[{"t":"parse-server-kit","u":"index.html","s":"","x":""},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html","s":"","x":"This is the part of Parse Server worth building on. Per-row authorisation, enforced by the database layer on every query, is weeks of work in any framework that does not have it — "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#model","s":"Two layers, and both must pass","x":"Parse checks permissions twice, at different granularities. Understanding which layer is refusing you is most of the debugging. CLP — class levelACL — row level QuestionMay this ki"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp","s":"Class-level permissions","x":"Six operations, declared once on the model and written into the schema at boot. import { ParseClass, roleKey, implementACL, cloneAcl, CloudFunction, catchError, BeforeSave, validat"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp-forms","s":"The four forms","x":"Written asMeans {}Master key only. No client reaches it — the strictest setting {requiresAuthentication: true}Any signed-in user {[roleKey(\u0027Editor\u0027)]: true}"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#protected-fields","s":"Hiding individual fields","x":"Sometimes the row should be readable but one column should not. protectedFields removes fields per role, without a second class or a mapping layer. @ParseClass(\u0027Employee\u0027, { clp: {"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#default-acl","s":"The default ACL template — read this one","x":"@ParseClass also writes a default ACL template into the schema. It is applied to objects created without an explicit ACL, and if you do not set it, it is: { \u0027*\u0027: { read: true, writ"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#acl","s":"Row-level ACLs","x":"An ACL is a map from an identity to what it may do. Three kinds of key: KeyIdentity *Everyone, signed in or not role:EditorEvery member of that role, resolved at request time"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#implementacl","s":"implementACL","x":"Takes one params object and returns an ACL. It does not take the object as an argument — a signature worth remembering, because the wrong shape is a common mistake. article.setACL("},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#patterns","s":"Patterns","x":"Most applications need four or five of these. They compose. The record belongs to its creator @CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-owner","s":"The record belongs to its creator","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async createNote(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const note = Note.fromP"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-status","s":"Visibility that follows a status","x":"function articleAcl(authorId: string, status: string) { return implementACL({ owner: [{ user: authorId, read: true, write: true }], roleRules: [{ role: Roles.ADMIN, read: true\u003c"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-team","s":"Scoping to a team or organisation","x":"Parse roles can contain other roles, so a per-tenant role gives you multi-tenancy without a where clause anywhere in the codebase. // One role per organisation: \u0027org:acme\u0027. Members"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-trigger","s":"Enforcing it on every save path","x":"An ACL set in one endpoint protects that endpoint. Setting it in @BeforeSave protects the dashboard, scripts, imports and any endpoint added later. @BeforeSave() static async onBef"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-nested","s":"Nested objects that must follow the parent","x":"A pointed-to record keeps whatever ACL it was created with. Publish the parent and its image stays invisible; hide the parent and its image stays readable. offer.setACL(implementAC"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#master-key","s":"The master key, and why to use it sparingly","x":"{useMasterKey: true} bypasses CLP and ACL completely. It is the right tool for administrative work, migrations and system jobs — and the wrong one for ordinary request handling, be"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#mistakes","s":"Common mistakes","x":"SymptomUsually A record is visible to everyoneNo ACL was ever set, so the class\u0027s default template applied — and unless you declared one, that is public read and write A query retu"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#debugging","s":"Working out which layer refused you","x":"Repeat the call with the master key. If it works, permissions are the cause; if not, look at the query. Read the row\u0027s ACL — obj.getACL()?.toJSON() shows exactly which identities h"},{"t":"AI assistants","u":"guide/ai-assistants.html","s":"","x":"This library\u0027s failure modes are, almost exactly, the mistakes a language model makes by default. That is not a coincidence — and it is fixable by handing your assistant the rules "},{"t":"AI assistants","u":"guide/ai-assistants.html#why","s":"Why this library specifically","x":"A model writing TypeScript reaches for the form that is correct nearly everywhere: @ParseField({ type: \u0027String\u0027, required: true }) title!: string; // correct in every other library"},{"t":"AI assistants","u":"guide/ai-assistants.html#generate","s":"Getting them","x":"psk new asks, before it writes anything: Which AI coding assistants do you use? • 1 Claude Code CLAUDE.md, plus two skills and a review agent 2 Cursor .cursor/rules/parse-server-ki"},{"t":"AI assistants","u":"guide/ai-assistants.html#existing","s":"A project that already exists","x":"Skipped the question, or changed tools since? psk ai writes into the project you are standing in, taking its name from package.json: psk ai # asks, same list psk ai claude cursor #"},{"t":"AI assistants","u":"guide/ai-assistants.html#unattended","s":"Unattended","x":"psk new my-api --ai=claude,agents -y psk new my-api --ai=none -y # nothing written --ai= wins over the prompt, and a non-interactive shell takes the default rather than hanging — s"},{"t":"AI assistants","u":"guide/ai-assistants.html#targets","s":"What each tool gets","x":"Assistant--aiFiles Claude Codeclaude CLAUDE.md.claude/skills/parse-model/SKILL.md.claude/skills/parse-endpoint/SKILL.md.claude/agents/parse-reviewer.md"},{"t":"AI assistants","u":"guide/ai-assistants.html#contents","s":"What is in them","x":"Ten rules, each one a silent failure, each shown with the wrong form next to the right one — a rule stated in the abstract is easy to misapply: RuleWhat happens when it is broken d"},{"t":"AI assistants","u":"guide/ai-assistants.html#index","s":"Knowing what exists","x":"The rules file also carries a one-line index of every export — models, endpoints, all 21 trigger types, cron, permissions, validation, transactions, locking, indexes, schema, middl"},{"t":"AI assistants","u":"guide/ai-assistants.html#skills","s":"Skills and the review agent","x":"Claude Code gets eight more files, because it can load instructions on demand rather than carrying everything in context. That split is deliberate: the rules file is read on every "},{"t":"AI assistants","u":"guide/ai-assistants.html#reference","s":"Pointing an assistant at the full API","x":"The generated files are the rules, not the reference. The complete API — every signature, taken from the source rather than paraphrased — ships inside the package: node_modules/par"},{"t":"AI assistants","u":"guide/ai-assistants.html#honest","s":"What this does not do","x":"Instructions raise the floor; they do not remove the need to read the diff. Two habits catch nearly everything that gets through: Run the build. The decorators are compile-time. A "},{"t":"The psk CLI","u":"guide/cli.html","s":"","x":"Create a project that already runs, and add entities to it without touching a registration file."},{"t":"The psk CLI","u":"guide/cli.html#commands","s":"Commands","x":"CommandDoes psk new \u003cname\u003eCreate a project that boots and serves psk g resource \u003cName\u003eAdd a model and five CRUD endpoints psk g model \u003cName\u003eAdd a model only"},{"t":"The psk CLI","u":"guide/cli.html#new","s":"Creating a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev Fifteen files, and every one of them removes a trap that is otherwise easy to hit: What it setsWh"},{"t":"The psk CLI","u":"guide/cli.html#ai","s":"It asks which assistant you use","x":"Before writing anything, because the same traps in that table are the ones a language model walks into by default — title!: string is correct in every other library and silently br"},{"t":"The psk CLI","u":"guide/cli.html#resource","s":"Adding an entity","x":"psk g resource Product ✓ src/models/Product.ts ✓ src/functions/product.ts Routes now available: POST /api/products/createProduct GET /api/products/listProducts GET /api/products/ge"},{"t":"The psk CLI","u":"guide/cli.html#layout","s":"It follows your layout","x":"The generator looks for where your models already live and writes alongside them, including a per-entity modules/{Name}/functions.ts arrangement. A fresh project gets models/ and f"},{"t":"Parse Dashboard","u":"guide/dashboard.html","s":"","x":"An admin console over every class, query, user and role — the thing you would otherwise build or buy. It is one install away and already wired into the generated project, but it is"},{"t":"Parse Dashboard","u":"guide/dashboard.html#what","s":"What you get","x":"Parse Dashboard is the official web console for a Parse Server. It is a real application, not a table viewer: AreaWhat it does BrowserEvery class as a spreadsheet — read, edit, add"},{"t":"Parse Dashboard","u":"guide/dashboard.html#install","s":"Getting it","x":"Two commands. Nothing to configure. npm install parse-dashboard npm run dev It appears at http://localhost:1337/dashboard. The generated app.ts already looks for it: /** * Mount Pa"},{"t":"Parse Dashboard","u":"guide/dashboard.html#why-not-bundled","s":"Why it is not bundled","x":"Three reasons, in order of how much they matter: It is a large install. The dashboard is a bundled React application with its own dependency tree — a few hundred packages that ever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#security","s":"It holds the master key","x":"This is the whole security story in one sentence The dashboard authenticates to your API with the master key, which bypasses every class-level permission and every row ACL. Whoever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#credentials","s":"Credentials","x":"# .env DASHBOARD_USER=admin DASHBOARD_PASS=something-long-and-unguessable DASHBOARD_PASSDevelopmentProduction set mounts, uses it \u003ctd"},{"t":"Parse Dashboard","u":"guide/dashboard.html#https","s":"HTTPS","x":"Parse Dashboard refuses to serve over plain HTTP unless you pass allowInsecureHTTP, because the master key would be travelling in clear. The template passes it only when NODE_ENV i"},{"t":"Parse Dashboard","u":"guide/dashboard.html#read-only","s":"Read-only operators","x":"Not everyone who needs to look at data needs to change it. Parse Server has a read-only master key, and the dashboard has read-only users to go with it: // ParseServer options read"},{"t":"Parse Dashboard","u":"guide/dashboard.html#middleware","s":"How it sits with the middleware","x":"The dashboard is mounted at /dashboard, outside MOUNT_PATH, so restrictRoutes never sees its requests. That is not a loophole: the dashboard talks to the API with the master key, a"},{"t":"Parse Dashboard","u":"guide/dashboard.html#production","s":"Exposing it in production","x":"In rough order of preference: Do not. Run it locally against the production serverURL when you need it, so there is no permanently reachable console at all. Behind a VPN or an auth"},{"t":"Parse Dashboard","u":"guide/dashboard.html#troubleshooting","s":"When it does not appear","x":"SymptomCause Banner says npm install parse-dashboard Not installed, or installed without restarting Banner says not mounted: set DASHBOARD_PASS NODE_ENV=pr"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html","s":"","x":"The method name is the route. No route table, no controller to keep in sync."},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#route","s":"@Route","x":"import { Route, CloudFunction, catchError, setupSwagger, } from \u0027parse-server-kit\u0027; @Route(Product) // -\u003e /api/products/* @Route(\u0027menu-items\u0027) /"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#cloudfunction","s":"@CloudFunction","x":"@CloudFunction({ methods: [\u0027POST\u0027], description: \u0027Create a product\u0027, validation: { requireUser: true, fields: { name: { required: true } } }, requireRoles: [\u0027Admin\u0027], rateLimit: { "},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#errors","s":"Error handling","x":"const [err, saved] = await catchError(product.save(null, { sessionToken })); if (err) throw err; catchError is the convention throughout the library: it turns a rejected promise in"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#openapi","s":"OpenAPI","x":"Every endpoint appears in the generated document with its real route, derived from @Route — no annotations needed. GET and HEAD emit query parameters; everything else emits a JSON "},{"t":"Errors","u":"guide/errors.html","s":"","x":"What to throw, what the client sees, and which code means what."},{"t":"Errors","u":"guide/errors.html#catcherror","s":"The convention","x":"This library uses catchError rather than try/catch around every await. It turns a rejected promise into a tuple, so the failure is a value you handle rather than a jump. import { c"},{"t":"Errors","u":"guide/errors.html#throwing","s":"Throwing something useful","x":"throw new Parse.Error(142, \u0027Price must be greater than zero\u0027); A Parse.Error reaches the client as {code, error} with a sensible HTTP status. Anything else becomes a generic 141 wi"},{"t":"Errors","u":"guide/errors.html#validation","s":"Validation errors","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // 142, listing every failure at once } validateOrThrow checks e"},{"t":"Errors","u":"guide/errors.html#conflicts","s":"Write conflicts","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Errors","u":"guide/errors.html#responses","s":"What the client receives","x":"removeResultMiddleware unwraps Parse\u0027s {result: …} envelope on success. Errors pass through unchanged: // success { \"objectId\": \"abc123\", \"name\": \"Widget\" } // failure { \u003c"},{"t":"Errors","u":"guide/errors.html#practice","s":"In practice","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async publishArticle(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const [findErr, art"},{"t":"Errors","u":"guide/errors.html#logging","s":"Logging","x":"parse-server logs an uncaught error from a cloud function with the function name, the parameters and the user. Deliberate Parse.Error throws are logged too, so a noisy validation p"},{"t":"Files \u0026 images","u":"guide/files.html","s":"","x":"Uploading, storing and serving files — and the ACL problem that catches everyone the first time."},{"t":"Files \u0026 images","u":"guide/files.html#parsefile","s":"Parse.File","x":"A file is uploaded first and attached second. It has its own lifecycle and its own URL. const file = new Parse.File(\u0027invoice.pdf\u0027, { base64 }, \u0027application/pdf\u0027); await file.save({"},{"t":"Files \u0026 images","u":"guide/files.html#field","s":"Declaring a file field","x":"import { ParseField, implementACL, syncImageAcl, CloudFunction, catchError, BeforeSaveFile, AfterDeleteFile, AfterDelete, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027File\u0027 }) d"},{"t":"Files \u0026 images","u":"guide/files.html#acl","s":"The problem everyone hits","x":"A file\u0027s URL is not protected by the parent record\u0027s ACL. Anyone holding the URL can fetch the bytes. What the ACL protects is the row that points at it. A file URL is effectively "},{"t":"Files \u0026 images","u":"guide/files.html#cascade","s":"Making images follow their parent","x":"When you use a wrapper class, its row has its own ACL — and that ACL does not update itself when the parent\u0027s visibility changes. Publish an article and its cover stays invisible; "},{"t":"Files \u0026 images","u":"guide/files.html#upload-endpoint","s":"An upload endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { base64: { required: true }, fileName: { required: true }, } }, }) static async uploadImage(req: Parse"},{"t":"Files \u0026 images","u":"guide/files.html#triggers","s":"File triggers","x":"@BeforeSaveFile() static async onBeforeSaveFile(req: any) { const { file, user } = req; if (!user) throw new Parse.Error(119, \u0027Sign in to upload\u0027);"},{"t":"Files \u0026 images","u":"guide/files.html#cleanup","s":"Deleting","x":"Deleting a row that points at a file does not delete the file. Storage keeps it until something removes it. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRe"},{"t":"Files \u0026 images","u":"guide/files.html#storage","s":"Where files actually live","x":"AdapterSuits Filesystem (default)Development. Lost on redeploy, not shared between instances S3, Google Cloud StorageProduction GridFSKeeping files in MongoDB — simple, but grows t"},{"t":"Files \u0026 images","u":"guide/files.html#mistakes","s":"Common mistakes","x":"SymptomCause An image is invisible while its record is publicsyncImageAcl not called after the parent\u0027s ACL was set An image stays visible after the record was hiddenSame, on the t"},{"t":"fromParams","u":"guide/from-params.html","s":"","x":"Turns a request body into a typed Parse object, reading your @ParseField declarations to convert pointers, dates and geopoints. It is designed to be pointed straight at raw user in"},{"t":"fromParams","u":"guide/from-params.html#why","s":"The alternative","x":"Without it, every create and update is a run of set() calls that drift from the model each time a field is added: // Twelve lines that must be edited every time the model changes, "},{"t":"fromParams","u":"guide/from-params.html#conversions","s":"What it converts","x":"It walks your declared fields and converts each according to its type. Declared typeSent asBecomes String, Number, Boolean\"Widget\", 12the value, unchanged"},{"t":"fromParams","u":"guide/from-params.html#clearing","s":"Clearing a pointer","x":"Both null and {} mean remove this relationship, so a form that submits an empty select works without special handling: { \"category\": null } // cleared { \"category\": {} } // cleared"},{"t":"fromParams","u":"guide/from-params.html#files","s":"File and image fields are skipped","x":"Pointer fields whose target is an excluded class — IMG and File by default — are deliberately not converted. An uploaded file has to be saved and processed before it can be attache"},{"t":"fromParams","u":"guide/from-params.html#create-update","s":"One call for create and update","x":"// No id in the body -\u003e insert Product.fromParams({ name: \u0027New\u0027 }); // id present -\u003e the same call updates that row Product.fromParams({ id: \u0027prod789\u0027, name: \u0027Edited\u0027 }); That is w"},{"t":"fromParams","u":"guide/from-params.html#safety","s":"Why it is safe on raw input","x":"The obvious worry about mapping a request body onto a model is over-posting: a caller adding {\"isAdmin\": true} and having it stored. That does not happen, and the reason is worth k"},{"t":"fromParams","u":"guide/from-params.html#control","s":"What you do still control","x":"Two things are worth being deliberate about. Neither is a flaw — both are the natural consequence of mapping a body onto a model. Server-controlled fields If a field is declared, a"},{"t":"fromParams","u":"guide/from-params.html#server-fields","s":"Server-controlled fields","x":"If a field is declared, a caller can set it. A status, isApproved or ownerId that your server means to own will be taken from the body if it is there. const product = Product.fromP"},{"t":"fromParams","u":"guide/from-params.html#clp-switch","s":"An id changes which permission applies","x":"Because an id in the body turns the save into an update, Parse checks the update permission rather than create — the row\u0027s ACL write access, and the class\u0027s update CLP. That protec"},{"t":"fromParams","u":"guide/from-params.html#acl-in-body","s":"An ACL sent in the body","x":"A caller can include ACL in the payload. It is not saved — like any undeclared value it stays clean — but getACL() will return it until you set your own. That only matters in one s"},{"t":"fromParams","u":"guide/from-params.html#complete","s":"A complete create endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { name: { required: true } } }, }) static async createProduct(req: Parse.Cloud.FunctionRequest) { const"},{"t":"fromParams","u":"guide/from-params.html#notes","s":"Smaller things","x":"It mutates the params objectSets className on the input so Parse can build from it. Harmless for req.params, surprising if you reuse the same object Absent means untouchedA declare"},{"t":"fromParams","u":"guide/from-params.html#summary","s":"In short","x":"Converts pointers, arrays of pointers, dates and geopoints from your declarations. Never writes a field you did not declare, whatever the caller sends. Skips file and image pointer"},{"t":"Getting started","u":"guide/getting-started.html","s":"","x":"Three commands to a running API with OpenAPI docs, a seeded user, and a MongoDB configured so transactions actually work."},{"t":"Getting started","u":"guide/getting-started.html#requirements","s":"Requirements","x":"Node≥ 20.19 MongoDB≥ 7.0.16 for parse-server 9. A replica set is required for transactions TypeScriptAny version, with experimentalDecorators: t"},{"t":"Getting started","u":"guide/getting-started.html#create","s":"Create a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev The generated project prints a ready-to-paste curl on startup, including a session token for "},{"t":"Getting started","u":"guide/getting-started.html#what-you-get","s":"What was generated","x":"my-api/ ├── docker-compose.yml # MongoDB as a replica set ├── tsconfig.json # decorator flags already set ├── .env └── src/ ├── app.ts # boot order — the sequence matters ├── seed."},{"t":"Getting started","u":"guide/getting-started.html#first-request","s":"Your first request","x":"Both of these are real exchanges against a freshly generated project — the responses below are what the server actually returned. Create — needs the session token printed at startu"},{"t":"Getting started","u":"guide/getting-started.html#dashboard","s":"Parse Dashboard","x":"Parse has an official admin console \u0026mdash; browse and edit every class, run queries, inspect users and roles, send push. It is not installed by default, and getting it is one comm"},{"t":"Getting started","u":"guide/getting-started.html#adding","s":"Adding your own entity","x":"psk g resource Product Writes src/models/Product.ts and src/functions/product.ts with the edit points marked, and changes nothing else — there is no module to register into, becaus"},{"t":"Getting started","u":"guide/getting-started.html#tsconfig","s":"TypeScript setup","x":"If you are adding this to an existing project rather than generating one, one compiler flag is mandatory: { \"compilerOptions\": { \"experimentalDecorators\": true } } This library use"},{"t":"Getting started","u":"guide/getting-started.html#parse-docs","s":"Where Parse\u0027s own docs take over","x":"This site covers what this library adds. For Parse Server itself — query operators, ACL semantics, LiveQuery, file adapters, push, authentication providers — the Parse Platform doc"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html","s":"","x":"Clients subscribe to a query and receive updates as rows change — with the same ACL deciding who sees what."},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#what","s":"What it is","x":"A client sends a query and keeps the connection open. When a row that matches is created, changed or removed, the server pushes the event. No polling, no websocket layer to design,"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#enable","s":"Enabling it","x":"Two settings on Parse Server: which classes may be subscribed to, and the websocket server itself. const parseServer = ParseServer({ // … your usual options … liveQuery: { classNam"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#client","s":"Subscribing, from a client","x":"const query = new Parse.Query(Task); query.equalTo(\u0027project\u0027, Project.pointer(projectId)); query.equalTo(\u0027done\u0027, false); const subscription = await query.subscribe(); subscriptio"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#permissions","s":"Permissions","x":"A subscription carries the client\u0027s session token, and every event is checked against the row\u0027s ACL before it is sent. Two users subscribed to the same query receive different even"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#triggers","s":"Server-side hooks","x":"import { ParseClass, BaseModel, BeforeConnect, BeforeSubscribe, AfterEvent, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Task\u0027) export default class Task extends BaseModel { \u003cspan"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#cost","s":"What it costs","x":"Every save on a live class is matched against every open subscription for it. Keep classNames to the classes that genuinely need it. Connections are stateful — they hold memory and"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#alternatives","s":"When not to use it","x":"NeedBetter Notify a user who is not on the pagePush notification Refresh a dashboard every minutePolling — simpler and cheaper Stream a high-frequency feedA dedicated channel; Live"},{"t":"Models \u0026 fields","u":"guide/models.html","s":"","x":"One class produces the schema, the indexes, the database validators, the permissions and the OpenAPI definition."},{"t":"Models \u0026 fields","u":"guide/models.html#parseclass","s":"@ParseClass","x":"Registers the Parse subclass and everything derived from it. Applied to a class extending BaseModel. import { ParseClass, BaseModel, ParseField, BeforeSave, validateOrThrow, } from"},{"t":"Models \u0026 fields","u":"guide/models.html#parsefield","s":"@ParseField","x":"Defines a getter and setter on the prototype that read and write Parse\u0027s attribute store. That means product.name is the supported API, not product.get(\u0027name\u0027). @ParseField({ type:"},{"t":"Models \u0026 fields","u":"guide/models.html#field-options","s":"Options","x":"OptionApplies toEffect typeallString, Number, Boolean, Date, Object, Array, GeoPoint, File, Bytes, Polygon, Pointer, Relation requiredallEnforced by validateOrThrow and by the data"},{"t":"Models \u0026 fields","u":"guide/models.html#basemodel","s":"BaseModel","x":"// A reference by id, with no fetch. const ref = Category.pointer(\u0027abc123\u0027); // Build a typed instance from request params, reading @ParseField // metadata to convert pointers, arr"},{"t":"Models \u0026 fields","u":"guide/models.html#validation","s":"Validation","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // checks every rule declared above } validateOrThrow checks req"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html","s":"","x":"One line on a model, and a stale save is refused instead of silently overwriting."},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#problem","s":"The problem","x":"Two people open the same record, both save. The second write overwrites the first, and neither is told. Guarding against that in every endpoint means remembering to, every time, fo"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#usage","s":"The whole feature","x":"import { ParseClass, BaseModel, ParseVersionField, createVersionedMongoAdapter, VersionRegistry, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Job\u0027) class Job extends BaseModel {"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#requirements","s":"Requirements","x":"Needs createVersionedMongoAdapter, the same adapter that powers transactions. It is MongoDB only; on Postgres the field is declared but nothing enforces it — and since 3.0 that say"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#handling","s":"Recovering","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { const\u003c/span"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#registry","s":"Inspecting it","x":"VersionRegistry.classNames(); // every versioned class VersionRegistry.isVersioned(\u0027Job\u0027); // true VersionRegistry.adapterIsInstalled(); BackTransactions \u003ca class=\"next\" href=\"seed"},{"t":"Queries","u":"guide/queries.html","s":"","x":"Finding things. Filters, sorting, pagination, pointers, sub-queries and counting — with the permission model doing the filtering you would otherwise write by hand."},{"t":"Queries","u":"guide/queries.html#basics","s":"The shape of a query","x":"Build it, constrain it, run it. Always pass a sessionToken so the ACL applies — see permissions below. import {catchError, ParseClass, CloudFunction} from \u0027parse-server-kit\u0027; const"},{"t":"Queries","u":"guide/queries.html#filters","s":"Filtering","x":"MethodFinds rows where equalTo(k, v)the field equals v notEqualTo(k, v)it does not greaterThan(k, v) / lessThan(k, v)numeric or date comparison"},{"t":"Queries","u":"guide/queries.html#search","s":"Text search","x":"// Case-insensitive substring. Simple, but it cannot use an index - // fine for admin screens, poor for large public listings. query.matches(\u0027name\u0027, new RegExp(search, \u0027i\u0027)); // Pr"},{"t":"Queries","u":"guide/queries.html#combining","s":"Combining conditions","x":"Conditions on one query are combined with AND. For OR, build several queries and join them. const byName = new Parse.Query(Product); byName.matches(\u0027name\u0027, new RegExp(term, \u0027i\u0027)); "},{"t":"Queries","u":"guide/queries.html#pointers","s":"Pointers and related data","x":"A Pointer stores only a reference. Ask for the related row explicitly, or you get {__type, className, objectId} and nothing else. // Fetch the category along with each product quer"},{"t":"Queries","u":"guide/queries.html#sorting","s":"Sorting and pagination","x":"query.descending(\u0027createdAt\u0027); // newest first query.ascending(\u0027name\u0027); // A to Z query.addDescending(\u0027price\u0027); // then by price, within that const\u003c/spa"},{"t":"Queries","u":"guide/queries.html#deep-paging","s":"Paging deeply","x":"skip gets slower the further in you go, because the database still walks the skipped rows. For a large export, page on a sort key instead: let cursor: Date | undefined; while (true"},{"t":"Queries","u":"guide/queries.html#single","s":"Fetching one row","x":"// By id. Throws 101 when missing OR when the ACL hides it. const [err, product] = await catchError(query.get(id, { sessionToken })); // First match, or undefined - no throw const "},{"t":"Queries","u":"guide/queries.html#counting","s":"Counting","x":"const [err, total] = await catchError(query.count({ sessionToken })); // A list endpoint usually wants both, from the same constraints. const rows = await query.find({ sessionToken"},{"t":"Queries","u":"guide/queries.html#permissions","s":"Queries and permissions","x":"This is the part that removes work rather than adding it. // Filtered by the ACL - only rows this user may read come back. await query.find({ sessionToken: user.getSessionToken() }"},{"t":"Queries","u":"guide/queries.html#aggregate","s":"Aggregation and distinct","x":"// Master key required for both. const distinct = await query.distinct(\u0027status\u0027); const pipeline = [ { match: { status: \u0027active\u0027 } }, { group: { objectId: \u0027$category\u0027, total:"},{"t":"Queries","u":"guide/queries.html#patterns","s":"A complete list endpoint","x":"@CloudFunction({ methods: [\u0027GET\u0027], description: \u0027List products\u0027, validation: { fields: { page: { type: String }, limit: { type: String }, status: { type: String }, search: { type: "},{"t":"Queries","u":"guide/queries.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed property access is unavailableThe result was not cast — rows as Product[] A pointer field is {__type: \u0027Pointer\u0027}No include() for it Only 100 rows come backThat"},{"t":"Relationships","u":"guide/relations.html","s":"","x":"Pointer, array of pointers, or Relation — three ways to connect records, and the one question that decides between them."},{"t":"Relationships","u":"guide/relations.html#three","s":"Three options","x":"HoldsGood up toQuery from Pointerone reference—either side Array of pointersa list, stored on the row~100either side Relationa list, stored sep"},{"t":"Relationships","u":"guide/relations.html#pointer","s":"Pointer — one reference","x":"import { ParseField, ParseClass, BaseModel, BeforeSave, cloneAcl, AfterDelete, catchError, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Category\u0027, requir"},{"t":"Relationships","u":"guide/relations.html#one-to-many","s":"One to many","x":"Put the pointer on the many side. A project does not hold a list of tasks; each task points at its project. // Task @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Project\u0027, required:"},{"t":"Relationships","u":"guide/relations.html#array","s":"Array of pointers — a small curated list","x":"// targetClass is what makes fromParams convert the ids @ParseField({ type: \u0027Array\u0027, targetClass: \u0027Tag\u0027 }) declare tags: any[]; { \"tags\":"},{"t":"Relationships","u":"guide/relations.html#relation","s":"Relation — a large list","x":"Stored outside the row, so size does not affect it and adding does not rewrite the parent. @ParseField({ type: \u0027Relation\u0027, targetClass: \u0027User\u0027 }) declare members: any; \u003cspan class="},{"t":"Relationships","u":"guide/relations.html#join","s":"A join model — when the link has its own data","x":"The moment the relationship needs attributes of its own — a role, a joined-at date, an order — it is a record, not a pointer. @ParseClass(\u0027Membership\u0027) export default class Members"},{"t":"Relationships","u":"guide/relations.html#querying","s":"Querying across a relationship","x":"// Tasks whose project is archived - a sub-query const archived = new Parse.Query(Project).equalTo(\u0027status\u0027, \u0027archived\u0027); new Parse.Query(Task).matchesQuery(\u0027project\u0027, archived);"},{"t":"Relationships","u":"guide/relations.html#acl","s":"Permissions across a relationship","x":"A related row has its own ACL. A task is not private because its project is — you have to say so. The usual place is the child\u0027s @BeforeSave: @BeforeSave() static async onBeforeSav"},{"t":"Relationships","u":"guide/relations.html#deleting","s":"Deleting","x":"Parse does not cascade. Deleting a project leaves its tasks pointing at nothing. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRequest\u003cProject\u003e) { const que"},{"t":"Relationships","u":"guide/relations.html#summary","s":"Choosing, briefly","x":"RelationshipUse Task → its projectPointer on the task Project → its tasksNo field. Query tasks by project Post → its tagsArray of pointers Team → its members (many)Relation, or a j"},{"t":"Seeding data","u":"guide/seeding.html","s":"","x":"A fresh install has no roles, so every role-gated endpoint refuses everyone — including you. Seeding is what makes an empty database into a working one, and the generated project s"},{"t":"Seeding data","u":"guide/seeding.html#why","s":"Why an empty database is not a working one","x":"Nothing about this is obvious until it happens. You start the server, everything logs green, and the first request fails: The first POST anyone tries POST /api/notes/createNote X-P"},{"t":"Seeding data","u":"guide/seeding.html#two-kinds","s":"Two kinds of seed, and why they are separate","x":"Almost every seeding mistake comes from treating these as one thing: Reference dataSample data What it isroles, the first admin, currencies, countriesdemo users, example rows Witho"},{"t":"Seeding data","u":"guide/seeding.html#idempotent","s":"Write it so you can run it twice","x":"A seed you are afraid to re-run is a seed that drifts out of step with the code, and you find that out during an incident. Every helper in the template finds first and creates only"},{"t":"Seeding data","u":"guide/seeding.html#roles","s":"Role hierarchy: the direction everyone gets backwards","x":"Making an Admin count as an Editor is one line, and it is the reverse of what reads naturally. Adding role A to role B\u0027s roles relation means every member of A is also treated as a"},{"t":"Seeding data","u":"guide/seeding.html#membership","s":"Adding a user to a role is a save on the role","x":"Role membership is a Parse relation, so the write happens on the role, not the user. This trips people up because setting a role field on the user looks like it should work — and i"},{"t":"Seeding data","u":"guide/seeding.html#running","s":"Two ways to run it","x":"At bootnpm run seed How it reaches the databasein process, directlyover REST, to a running server Server must be upno — it is the serveryes Runs"},{"t":"Seeding data","u":"guide/seeding.html#login-gotcha","s":"Why the standalone seed cannot log in","x":"One real quirk, worth knowing before it puzzles you. The template\u0027s sample seed finishes by logging in as the demo user so the startup banner can print a working session token. Run"},{"t":"Seeding data","u":"guide/seeding.html#output","s":"What a run looks like","x":"$ npm run seed Seed complete. roles created Editor, Admin users created admin, demo notes created 2 And immediately again, unchanged: $ npm run seed Seed complete. roles created no"},{"t":"Seeding data","u":"guide/seeding.html#extending","s":"Adding your own","x":"Seed rows are ordinary model instances, so everything the rest of the documentation says applies — including setting an ACL at creation rather than leaving rows world-readable: for"},{"t":"Seeding data","u":"guide/seeding.html#checklist","s":"Before you ship it","x":"Run it twice. The second run must create nothing. Run it against an empty database. It must succeed from nothing. ADMIN_PASSWORD set outside development. Sample data behind the NOD"},{"t":"Transactions","u":"guide/transactions.html","s":"","x":"Everything the body writes lands together, or none of it does — without threading a session through every call."},{"t":"Transactions","u":"guide/transactions.html#setup","s":"Setup","x":"Transactions are powered by a database adapter. Pass it to Parse Server instead of a plain URI: import { createVersionedMongoAdapter, CloudFunction, Transactional, withTransaction,"},{"t":"Transactions","u":"guide/transactions.html#usage","s":"Using it","x":"@CloudFunction({ methods: [\u0027POST\u0027] }) @Transactional() static async placeOrder(req: Parse.Cloud.FunctionRequest) { await order.save(null, { useMasterKey: true }); \u003cspan class=\""},{"t":"Transactions","u":"guide/transactions.html#semantics","s":"What it guarantees","x":"Nested calls join the outer transaction; the outermost caller commits. The body may re-run — up to three attempts on a transient conflict — so it must be safe to repeat. After thre"},{"t":"Transactions","u":"guide/transactions.html#conflicts","s":"Handling a conflict","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Triggers","u":"guide/triggers.html","s":"","x":"All 21 Parse trigger types as decorators, declared next to the thing they act on."},{"t":"Triggers","u":"guide/triggers.html#usage","s":"Declaring a trigger","x":"import { ParseClass, BaseModel, BeforeSave, Cron, CronSchedule, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Product\u0027) export default class Product extends BaseModel { \u003cspan class="},{"t":"Triggers","u":"guide/triggers.html#types","s":"Every trigger","x":"DecoratorRegisters as @BeforeSave @AfterSavebeforeSave / afterSave @BeforeDelete @AfterDeletebeforeDelete / afterDelete \u003cco"},{"t":"Triggers","u":"guide/triggers.html#cron","s":"Scheduled jobs","x":"class Jobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Nightly cleanup\u0027 }) static async cleanup() { … } } CronSchedule covers the usual patterns from EVERY_MINUT"},{"t":"Troubleshooting","u":"guide/troubleshooting.html","s":"","x":"Symptoms that have no error message attached, and where to look for each."},{"t":"Troubleshooting","u":"guide/troubleshooting.html#index","s":"Symptom index","x":"SymptomCauseWhere Decorators do nothing, or behave oddlyexperimentalDecorators not setGetting started — now throws with a clear message No models, empty schema, no routes"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#fixed","s":"Traps that no longer exist","x":"Two were removed rather than documented, because a warning is not a fix: Fields reading undefined A class field could shadow @ParseField\u0027s accessor at target: ES2022 — reads return"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#checklist","s":"When something will not start","x":"Is experimentalDecorators set? Did importFiles print the files it loaded — and are they the compiled ones? Did the registries initialise after Parse Server was mounted? Read the bo"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html","s":"","x":"A checkout API, where getting it wrong costs real money. Pointers and dates decoded straight from the request body, a total the client cannot forge, stock two shoppers cannot both "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#building","s":"What you are building","x":"A storefront back end with four jobs, in order of how badly they go wrong when done naively: JobThe naive versionWhat you will write Decode the request twenty set() calls that drif"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#setup","s":"1. Set up","x":"npx parse-server-kit new shop-api cd shop-api npm run db:up # MongoDB as a replica set - transactions need one npm run dev The replica set matters. Transactions "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#products","s":"2. Products, with stock worth protecting","x":"psk g resource Product Then fill in src/models/Product.ts: import { ParseClass, ParseField, BaseModel, ParseVersionField, roleKey, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Pr"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#order-model","s":"3. The order model, and every type fromParams decodes","x":"This model exists partly to be realistic and partly to cover, in one place, each field type that fromParams converts for you. @ParseClass(\u0027Order\u0027, { description: \u0027A customer order\u0027"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#placing","s":"4. Placing an order — what the browser actually sends","x":"Here is the whole point of fromParams. The frontend sends flat JSON: ids as strings, dates as strings, a geopoint as a plain object. Your model says what those fields are. fromPara"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#not-fromparams","s":"What fromParams deliberately does not do","x":"It is worth being precise, because the gaps are design decisions rather than omissions: InputResultWhy A key with no @ParseField ignored Only declared fields are set, so an unexpec"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#pricing","s":"5. Never trust the client\u0027s total","x":"The request deliberately did not include a price, and if it had, you would throw it away. Prices come from the database, every time. import { catchError } from \u0027parse-server-kit\u0027; "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#stock","s":"6. Two shoppers, one unit of stock","x":"This is the step that separates a demo from a system. Two requests read stock: 1 at the same moment, both check \"is there enough?\", both say yes, and both write stock: 0. You have "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#conflict","s":"What the loser sees","x":"Two requests, same instant POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"quantity\": 1 }] } POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"q"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#permissions","s":"7. Permissions that follow the order\u0027s status","x":"Now the part that a where clause cannot do. The rule is: the customer may always read their own order; they may edit it only while it is pending; staff may always read and write; n"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#transitions","s":"8. Status transitions, gated by role","x":"A customer must not mark their own order shipped. That is not an ACL question — they legitimately have write access while it is pending — it is a rule about which change is allowed"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#realtime","s":"9. Watching an order in real time","x":"The customer\u0027s page should update itself when the order ships, without polling. Parse\u0027s LiveQuery does that, and — the part that matters — it respects the ACL you already wrote. A "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#whole-flow","s":"10. The whole flow, on the wire","x":"Start to finish, this is every exchange a checkout makes: 1 · Browse GET /api/products/listProducts?limit=20 Public, no token needed { \u003cspan class=\""},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#built","s":"What you built","x":"ProblemWhat handles itLines you wrote Decoding pointers, dates, geopoints, id arraysOrder.fromParams()1 Forged totalsserver-side pricinga helper Overselling under load"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html","s":"","x":"A small task-tracking API, end to end: models, relationships, permissions that scope data per user, search, and a scheduled job. About twenty minutes."},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#setup","s":"1. Start the project","x":"npx parse-server-kit new tasks-api cd tasks-api npm run db:up npm run dev You now have a running server with a Note example, docs at /api-docs, and a seeded demo"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#model","s":"2. The first model","x":"Delete the example and generate your own: psk g resource Project Open src/models/Project.ts and fill in the fields: import { ParseClass, BaseModel, ParseField, BeforeSave, validate"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#ownership","s":"3. Make projects private to their owner","x":"The CLP above lets any signed-in user reach the class. The ACL decides which rows they see. Set it when the project is created — in src/functions/project.ts: static async createPro"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#relationship","s":"4. A second model, related to the first","x":"psk g resource Task @ParseField({ type: \u0027String\u0027, required: true }) declare title: string; @ParseField({ type: \u0027Boolean\u0027 }) \u003cspan class="},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#inherit-acl","s":"Tasks should follow their project","x":"A task is as private as the project it belongs to. Do that in the trigger, so it holds on every save path: @BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveReques"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#listing","s":"5. A list endpoint that filters and paginates","x":"@CloudFunction({ methods: [\u0027GET\u0027], validation: { fields: { project: { type: String }, done: { type: String }, search: { type: String }, page: { type: String }, } }, }) static async"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#cron","s":"6. A scheduled job","x":"Create src/cron.ts and import it from app.ts before the registries initialise: class TaskJobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Flag overdue tasks\u0027 }) "},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#try","s":"7. Try it","x":"export APP=tasks-api export TOK=\u003cthe token printed at startup\u003e curl -X POST localhost:1337/api/projects/createProject \\ -H \"X-Parse-Application-Id: $APP\" -H \"X-Parse-Session-Token:"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#what-next","s":"What you built","x":"Two models with a relationshipschema, indexes and validators derived from them Ten REST endpointsrouted from method names, documented automatically Per-row permissionsenforced by t"},{"t":"Users \u0026 authentication","u":"guide/users.html","s":"","x":"Signup, login, sessions, roles and password reset — the part you would otherwise spend a fortnight building."},{"t":"Users \u0026 authentication","u":"guide/users.html#why","s":"What you get without writing it","x":"Parse ships a _User class, hashed passwords, session tokens with revocation, email verification, password reset, and a _Role system that the ACL layer already understands. None of "},{"t":"Users \u0026 authentication","u":"guide/users.html#model","s":"Extending the user","x":"Add your own fields by declaring a model against the built-in class. import { ParseClass, ParseField, CloudFunction, catchError, invalidateRoles, getUserRoles, getUsersRoles, Befor"},{"t":"Users \u0026 authentication","u":"guide/users.html#signup","s":"Signing up","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { fields: { username: { required: true }, password: { required: true }, email: { required: true }, } }, }) static async signUp(req: "},{"t":"Users \u0026 authentication","u":"guide/users.html#login","s":"Logging in","x":"const [err, user] = await catchError( Parse.User.logIn(req.params.username, req.params.password) ); if (err) throw err; // 101 for wrong username or password return { sessionToken:"},{"t":"Users \u0026 authentication","u":"guide/users.html#sessions","s":"Sessions","x":"A session token identifies the caller on every later request. Clients send it as a header: X-Parse-Application-Id: my-api X-Parse-Session-Token: r:8f2c… Inside a cloud function it "},{"t":"Users \u0026 authentication","u":"guide/users.html#roles","s":"Roles","x":"A role is a named set of users, and it is what an ACL and a CLP refer to. // Create once, at setup or in a seed. const acl = new Parse.ACL(); acl.setPublicReadAccess(true); // so A"},{"t":"Users \u0026 authentication","u":"guide/users.html#password-reset","s":"Password reset and email","x":"await Parse.User.requestPasswordReset(email); Parse sends the mail, hosts the reset page and expires the link. It needs an email adapter and a publicServerURL configured on the ser"},{"t":"Users \u0026 authentication","u":"guide/users.html#protecting","s":"Protecting user data","x":"_User rows are readable by default in many setups. Two things to decide deliberately: @ParseClass(\u0027_User\u0027, { clp: { find: { requiresAuthentication: true }, get: { requiresAuthentic"},{"t":"Users \u0026 authentication","u":"guide/users.html#hooks","s":"Reacting to auth events","x":"@BeforeLogin() static async onBeforeLogin(req: any) { if (req.object.get(\u0027suspended\u0027)) { throw new Parse.Error(101, \u0027Account suspended\u0027); } } \u003cs"},{"t":"Users \u0026 authentication","u":"guide/users.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed fields on req.user are unavailableNot cast — req.user! as User A user can edit another userSaved with useMasterKey instead of the caller\u0027s session token Password"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html","s":"","x":"They are not really competitors. One gives you structure and you build the backend; the other is a backend you customise. Here is the honest side-by-side."},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#difference","s":"The actual difference","x":"NestJS is an application framework. It gives you dependency injection, module boundaries and a request lifecycle — and you build authentication, users, roles, permissions, real-tim"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#table","s":"Feature by feature","x":"NestJSparse-server-kit Auth, sessions, password resetyou build itbuilt in Users \u0026 rolesyou build itbuilt in Row"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#side-by-side","s":"The same feature, both ways","x":"\"Users may only see their own orders\" The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable("},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-permissions","s":"\"Users may only see their own orders\"","x":"The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable() export class OrdersService { constru"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-auth","s":"Signup and login","x":"// NestJS: install passport, jwt, bcrypt; write a User entity, an // AuthService, a JwtStrategy, guards, refresh tokens, a password // reset flow with tokens and expiry, and an ema"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-crud","s":"A CRUD resource","x":"// NestJS: nest g resource orders // orders.module.ts, orders.controller.ts, orders.service.ts, // dto/create-order.dto.ts, dto/update-order.dto.ts, // entities/order.entity.ts + r"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose","s":"When to choose NestJS","x":"Genuinely — these are good reasons, not concessions. You need a database Parse does not serve well. Postgres works, but transactions and optimistic locking here are MongoDB-only. Y"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose-parse","s":"When to choose this","x":"The app is mostly records with permissions. Business apps, admin tools, marketplaces, mobile backends — most software, honestly. Per-row authorisation is a real requirement. Multi-"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#honest","s":"The honest summary","x":"A fair way to decide Write down what your backend must do. Cross out everything Parse already provides — auth, users, roles, row-level permissions, real-time, files, push, dashboar"},{"t":"Configuration","u":"reference/configuration.html","s":"","x":"Settings, schema generation, indexes and the optional role cache."},{"t":"Configuration","u":"reference/configuration.html#configurekit","s":"configureKit","x":"Optional. Every default reproduces what the library did before these became settings, so a project that never calls it is unaffected. Values resolve when they are used, not at impo"},{"t":"Configuration","u":"reference/configuration.html#allowauthroutes","s":"allowAuthRoutes","x":"restrictRoutes closes Parse\u0027s generic REST API so clients go through the cloud functions you declared. Its auth endpoints are closed along with everything else, and the documented "},{"t":"Configuration","u":"reference/configuration.html#schema","s":"createSchemaConfig","x":"const schema = createSchemaConfig({ adminRole: \u0027Owner\u0027 }); // pass as the `schema` option to ParseServer OptionDefaultEffect adminRolefrom"},{"t":"Configuration","u":"reference/configuration.html#indexes","s":"Indexes","x":"await applyAllIndexes(parseServer); // after listen() await applyMongoValidators(parseServer); Declared asCreates @ParseField({unique: true})"},{"t":"Configuration","u":"reference/configuration.html#role-cache","s":"Role cache","x":"Off by default. requireRoles and getUserRoles() each cost a database round-trip. Role membership rarely changes, so it caches well — but a revoked role keeps working until the entr"},{"t":"Decorators","u":"reference/decorators.html","s":"","x":"Every decorator the package exports, with its signature."},{"t":"Decorators","u":"reference/decorators.html#model","s":"Model","x":"DecoratorTargetPurpose @ParseClass(name, options?)classRegisters the subclass, CLP, ACL template, compound indexes; registers with OpenAPI and flushes pending triggers @ParseField("},{"t":"Decorators","u":"reference/decorators.html#endpoints","s":"Endpoints","x":"@CloudFunction(config)static methodRegisters a cloud function with HTTP methods, validation and roles @ProtectedCloudFunction(config?)static methodAs above, with POST and requireUs"},{"t":"Decorators","u":"reference/decorators.html#triggers","s":"Triggers","x":"All take an optional {description?, validation?}. Object@BeforeSave @AfterSave @BeforeDelete @AfterDelete @BeforeFind @AfterFind Auth@BeforeLogin @AfterLogin @AfterL"},{"t":"Decorators","u":"reference/decorators.html#registries","s":"Registries","x":"Each is initialised once at boot, after Parse Server is mounted. CloudFunctionRegistryinitialize() also builds the route map and reports the versioning setup TriggerRegistryinitial"},{"t":"Decorators","u":"reference/decorators.html#hooks","s":"Registration hooks","x":"For integrations that need to observe registration as it happens. import { onClassRegistered, onFieldRegistered, onFunctionRegistered, } from \u0027parse-server-kit\u0027; onClassRegistered("},{"t":"Middleware","u":"reference/middleware.html","s":"","x":"What each middleware does, and the order they belong in."},{"t":"Middleware","u":"reference/middleware.html#order","s":"Boot order","x":"The sequence is load-bearing. Each step depends on the one before it. import { importFiles, CloudFunctionRegistry, TriggerRegistry, CronRegistry, setupSwagger, } from \u0027parse-server"},{"t":"Middleware","u":"reference/middleware.html#what","s":"What each one does","x":"MiddlewareResponsibility removeResultMiddlewareUnwraps Parse\u0027s {result: …} envelope so responses look like an ordinary REST API validateEntityRoutesResolves /api/{entity}/{action} "},{"t":"Middleware","u":"reference/middleware.html#security","s":"Security notes","x":"The master key is read from the X-Parse-Master-Key header. The request body is still accepted for compatibility and warns once — a secret in a body ends up in every log that record"},{"t":"Utilities","u":"reference/utilities.html","s":"","x":"Helpers, constants and types the package exports."},{"t":"Utilities","u":"reference/utilities.html#catcherror","s":"catchError","x":"The error convention throughout the library — a rejected promise becomes a tuple rather than a try/catch around every await. import { catchError, importFiles, getUserRoles, getUser"},{"t":"Utilities","u":"reference/utilities.html#importfiles","s":"importFiles","x":"importFiles(dir); // .js only importFiles(dir, { extensions: [\u0027.js\u0027, \u0027.ts\u0027] }); // ts-node / tsx importFiles(dir, { verbose: false }); Defaults to .js, the compile"},{"t":"Utilities","u":"reference/utilities.html#roles","s":"Roles","x":"await getUserRoles(user); // string[] await getUsersRoles(users); // Map roleKey(\u0027Editor\u0027); // \u0027role:Editor\u0027, typed as that literal getUsersRole"},{"t":"Utilities","u":"reference/utilities.html#misc","s":"Everything else","x":"syncImageAcl(parent, fields, acl?)Copies a parent\u0027s ACL onto nested image pointers cloneAcl(acl)Deep copy of a Parse.ACL validateObject(object){valid, errors[]} against the declare"},{"t":"Utilities","u":"reference/utilities.html#deprecated","s":"Deprecated","x":"Still exported so existing imports resolve; to be removed in the next major. UserRolesTwo example role names from one project. Declare your own enum and pass it to roleKey, which a"},{"u":"guide/api-docs.html","t":"API documentation","s":"What you get","x":"setupSwagger serves two things. The spec itself is always available at json; the browser UI needs swagger-ui-express, which the generated project installs for you. Call it after the registries have run, or the spec is generated before the endpoints exist and comes back nearly empty."},{"u":"guide/api-docs.html","t":"API documentation","s":"Where the content comes from","x":"Schemas from ParseClass and ParseField. Paths from Route plus the method name. Parameters or request body from validation fields. Security from requiresAuth, requireRoles, requireUser. Summaries, tags and responses from the swagger block on CloudFunction."},{"u":"guide/api-docs.html","t":"API documentation","s":"Tell the page your application id","x":"Every request needs X-Parse-Application-Id and a docs page that does not say which value is a guessing game. A wrong guess comes back as a bare unauthorized that names nothing. Pass appId to setupSwagger and the Authorize dialog shows it. The application id is not a secret."},{"u":"guide/api-docs.html","t":"API documentation","s":"The REST API key is opt-in","x":"Parse only enforces X-Parse-REST-API-Key when restAPIKey is configured, which most deployments do not do. The spec does not require it by default. Pass restApiKey true when you have one."},{"u":"guide/api-docs.html","t":"API documentation","s":"Without swagger-ui-express","x":"The UI package is optional. Without it setupSwagger logs a warning, skips the browser page and still serves the spec at api-docs json. If api-docs returns Cannot GET while api-docs json works, the package is simply not installed."},{"u":"guide/api-docs.html","t":"API documentation","s":"Every option","x":"setupSwagger takes app, config and an optional path that defaults to api-docs. Options are title, version, description, basePath, appId, restApiKey, host and schemes. The spec is always served at the path plus json."},{"u":"guide/api-docs.html","t":"API documentation","s":"Turning it on and off","x":"There is deliberately no enabled option. The way to not have documentation is to not mount it. The generated project reads DOCS_ENABLED and DOCS_PATH from env, so switching docs off is an environment variable rather than a code change, and the banner reports off instead of advertising a dead URL. Docs are on in production by default."}] \ No newline at end of file +[{"t":"parse-server-kit","u":"index.html","s":"","x":""},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html","s":"","x":"This is the part of Parse Server worth building on. Per-row authorisation, enforced by the database layer on every query, is weeks of work in any framework that does not have it — "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#model","s":"Two layers, and both must pass","x":"Parse checks permissions twice, at different granularities. Understanding which layer is refusing you is most of the debugging. CLP — class levelACL — row level QuestionMay this ki"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp","s":"Class-level permissions","x":"Six operations, declared once on the model and written into the schema at boot. import { ParseClass, roleKey, implementACL, cloneAcl, CloudFunction, catchError, BeforeSave, validat"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#clp-forms","s":"The four forms","x":"Written asMeans {}Master key only. No client reaches it — the strictest setting {requiresAuthentication: true}Any signed-in user {[roleKey(\u0027Editor\u0027)]: true}"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#protected-fields","s":"Hiding individual fields","x":"Sometimes the row should be readable but one column should not. protectedFields removes fields per role, without a second class or a mapping layer. @ParseClass(\u0027Employee\u0027, { clp: {"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#default-acl","s":"The default ACL template — read this one","x":"@ParseClass also writes a default ACL template into the schema. It is applied to objects created without an explicit ACL, and if you do not set it, it is: { \u0027*\u0027: { read: true, writ"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#acl","s":"Row-level ACLs","x":"An ACL is a map from an identity to what it may do. Three kinds of key: KeyIdentity *Everyone, signed in or not role:EditorEvery member of that role, resolved at request time"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#implementacl","s":"implementACL","x":"Takes one params object and returns an ACL. It does not take the object as an argument — a signature worth remembering, because the wrong shape is a common mistake. article.setACL("},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#patterns","s":"Patterns","x":"Most applications need four or five of these. They compose. The record belongs to its creator @CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async "},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-owner","s":"The record belongs to its creator","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async createNote(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const note = Note.fromP"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-status","s":"Visibility that follows a status","x":"function articleAcl(authorId: string, status: string) { return implementACL({ owner: [{ user: authorId, read: true, write: true }], roleRules: [{ role: Roles.ADMIN, read: true\u003c"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-team","s":"Scoping to a team or organisation","x":"Parse roles can contain other roles, so a per-tenant role gives you multi-tenancy without a where clause anywhere in the codebase. // One role per organisation: \u0027org:acme\u0027. Members"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-trigger","s":"Enforcing it on every save path","x":"An ACL set in one endpoint protects that endpoint. Setting it in @BeforeSave protects the dashboard, scripts, imports and any endpoint added later. @BeforeSave() static async onBef"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#pattern-nested","s":"Nested objects that must follow the parent","x":"A pointed-to record keeps whatever ACL it was created with. Publish the parent and its image stays invisible; hide the parent and its image stays readable. offer.setACL(implementAC"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#master-key","s":"The master key, and why to use it sparingly","x":"{useMasterKey: true} bypasses CLP and ACL completely. It is the right tool for administrative work, migrations and system jobs — and the wrong one for ordinary request handling, be"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#mistakes","s":"Common mistakes","x":"SymptomUsually A record is visible to everyoneNo ACL was ever set, so the class\u0027s default template applied — and unless you declared one, that is public read and write A query retu"},{"t":"ACL \u0026amp; permissions","u":"guide/acl.html#debugging","s":"Working out which layer refused you","x":"Repeat the call with the master key. If it works, permissions are the cause; if not, look at the query. Read the row\u0027s ACL — obj.getACL()?.toJSON() shows exactly which identities h"},{"t":"AI assistants","u":"guide/ai-assistants.html","s":"","x":"This library\u0027s failure modes are, almost exactly, the mistakes a language model makes by default. That is not a coincidence — and it is fixable by handing your assistant the rules "},{"t":"AI assistants","u":"guide/ai-assistants.html#why","s":"Why this library specifically","x":"A model writing TypeScript reaches for the form that is correct nearly everywhere: @ParseField({ type: \u0027String\u0027, required: true }) title!: string; // correct in every other library"},{"t":"AI assistants","u":"guide/ai-assistants.html#generate","s":"Getting them","x":"psk new asks, before it writes anything: Which AI coding assistants do you use? • 1 Claude Code CLAUDE.md, plus two skills and a review agent 2 Cursor .cursor/rules/parse-server-ki"},{"t":"AI assistants","u":"guide/ai-assistants.html#existing","s":"A project that already exists","x":"Skipped the question, or changed tools since? psk ai writes into the project you are standing in, taking its name from package.json: psk ai # asks, same list psk ai claude cursor #"},{"t":"AI assistants","u":"guide/ai-assistants.html#unattended","s":"Unattended","x":"psk new my-api --ai=claude,agents -y psk new my-api --ai=none -y # nothing written --ai= wins over the prompt, and a non-interactive shell takes the default rather than hanging — s"},{"t":"AI assistants","u":"guide/ai-assistants.html#targets","s":"What each tool gets","x":"Assistant--aiFiles Claude Codeclaude CLAUDE.md.claude/skills/parse-model/SKILL.md.claude/skills/parse-endpoint/SKILL.md.claude/agents/parse-reviewer.md"},{"t":"AI assistants","u":"guide/ai-assistants.html#contents","s":"What is in them","x":"Ten rules, each one a silent failure, each shown with the wrong form next to the right one — a rule stated in the abstract is easy to misapply: RuleWhat happens when it is broken d"},{"t":"AI assistants","u":"guide/ai-assistants.html#index","s":"Knowing what exists","x":"The rules file also carries a one-line index of every export — models, endpoints, all 21 trigger types, cron, permissions, validation, transactions, locking, indexes, schema, middl"},{"t":"AI assistants","u":"guide/ai-assistants.html#skills","s":"Skills and the review agent","x":"Claude Code gets eight more files, because it can load instructions on demand rather than carrying everything in context. That split is deliberate: the rules file is read on every "},{"t":"AI assistants","u":"guide/ai-assistants.html#reference","s":"Pointing an assistant at the full API","x":"The generated files are the rules, not the reference. The complete API — every signature, taken from the source rather than paraphrased — ships inside the package: node_modules/par"},{"t":"AI assistants","u":"guide/ai-assistants.html#honest","s":"What this does not do","x":"Instructions raise the floor; they do not remove the need to read the diff. Two habits catch nearly everything that gets through: Run the build. The decorators are compile-time. A "},{"t":"The psk CLI","u":"guide/cli.html","s":"","x":"Create a project that already runs, and add entities to it without touching a registration file."},{"t":"The psk CLI","u":"guide/cli.html#commands","s":"Commands","x":"CommandDoes psk new \u003cname\u003eCreate a project that boots and serves psk g resource \u003cName\u003eAdd a model and five CRUD endpoints psk g model \u003cName\u003eAdd a model only"},{"t":"The psk CLI","u":"guide/cli.html#new","s":"Creating a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev Fifteen files, and every one of them removes a trap that is otherwise easy to hit: What it setsWh"},{"t":"The psk CLI","u":"guide/cli.html#ai","s":"It asks which assistant you use","x":"Before writing anything, because the same traps in that table are the ones a language model walks into by default — title!: string is correct in every other library and silently br"},{"t":"The psk CLI","u":"guide/cli.html#resource","s":"Adding an entity","x":"psk g resource Product ✓ src/models/Product.ts ✓ src/functions/product.ts Routes now available: POST /api/products/createProduct GET /api/products/listProducts GET /api/products/ge"},{"t":"The psk CLI","u":"guide/cli.html#layout","s":"It follows your layout","x":"The generator looks for where your models already live and writes alongside them, including a per-entity modules/{Name}/functions.ts arrangement. A fresh project gets models/ and f"},{"t":"Parse Dashboard","u":"guide/dashboard.html","s":"","x":"An admin console over every class, query, user and role — the thing you would otherwise build or buy. It is one install away and already wired into the generated project, but it is"},{"t":"Parse Dashboard","u":"guide/dashboard.html#what","s":"What you get","x":"Parse Dashboard is the official web console for a Parse Server. It is a real application, not a table viewer: AreaWhat it does BrowserEvery class as a spreadsheet — read, edit, add"},{"t":"Parse Dashboard","u":"guide/dashboard.html#install","s":"Getting it","x":"Two commands. Nothing to configure. npm install parse-dashboard npm run dev It appears at http://localhost:1337/dashboard. The generated app.ts already looks for it: /** * Mount Pa"},{"t":"Parse Dashboard","u":"guide/dashboard.html#why-not-bundled","s":"Why it is not bundled","x":"Three reasons, in order of how much they matter: It is a large install. The dashboard is a bundled React application with its own dependency tree — a few hundred packages that ever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#security","s":"It holds the master key","x":"This is the whole security story in one sentence The dashboard authenticates to your API with the master key, which bypasses every class-level permission and every row ACL. Whoever"},{"t":"Parse Dashboard","u":"guide/dashboard.html#credentials","s":"Credentials","x":"# .env DASHBOARD_USER=admin DASHBOARD_PASS=something-long-and-unguessable DASHBOARD_PASSDevelopmentProduction set mounts, uses it \u003ctd"},{"t":"Parse Dashboard","u":"guide/dashboard.html#https","s":"HTTPS","x":"Parse Dashboard refuses to serve over plain HTTP unless you pass allowInsecureHTTP, because the master key would be travelling in clear. The template passes it only when NODE_ENV i"},{"t":"Parse Dashboard","u":"guide/dashboard.html#read-only","s":"Read-only operators","x":"Not everyone who needs to look at data needs to change it. Parse Server has a read-only master key, and the dashboard has read-only users to go with it: // ParseServer options read"},{"t":"Parse Dashboard","u":"guide/dashboard.html#middleware","s":"How it sits with the middleware","x":"The dashboard is mounted at /dashboard, outside MOUNT_PATH, so restrictRoutes never sees its requests. That is not a loophole: the dashboard talks to the API with the master key, a"},{"t":"Parse Dashboard","u":"guide/dashboard.html#production","s":"Exposing it in production","x":"In rough order of preference: Do not. Run it locally against the production serverURL when you need it, so there is no permanently reachable console at all. Behind a VPN or an auth"},{"t":"Parse Dashboard","u":"guide/dashboard.html#troubleshooting","s":"When it does not appear","x":"SymptomCause Banner says npm install parse-dashboard Not installed, or installed without restarting Banner says not mounted: set DASHBOARD_PASS NODE_ENV=pr"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html","s":"","x":"The method name is the route. No route table, no controller to keep in sync."},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#route","s":"@Route","x":"import { Route, CloudFunction, catchError, setupSwagger, } from \u0027parse-server-kit\u0027; @Route(Product) // -\u003e /api/products/* @Route(\u0027menu-items\u0027) /"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#cloudfunction","s":"@CloudFunction","x":"@CloudFunction({ methods: [\u0027POST\u0027], description: \u0027Create a product\u0027, validation: { requireUser: true, fields: { name: { required: true } } }, requireRoles: [\u0027Admin\u0027], rateLimit: { "},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#errors","s":"Error handling","x":"const [err, saved] = await catchError(product.save(null, { sessionToken })); if (err) throw err; catchError is the convention throughout the library: it turns a rejected promise in"},{"t":"Endpoints \u0026 routing","u":"guide/endpoints.html#openapi","s":"OpenAPI","x":"Every endpoint appears in the generated document with its real route, derived from @Route — no annotations needed. GET and HEAD emit query parameters; everything else emits a JSON "},{"t":"Errors","u":"guide/errors.html","s":"","x":"What to throw, what the client sees, and which code means what."},{"t":"Errors","u":"guide/errors.html#catcherror","s":"The convention","x":"This library uses catchError rather than try/catch around every await. It turns a rejected promise into a tuple, so the failure is a value you handle rather than a jump. import { c"},{"t":"Errors","u":"guide/errors.html#throwing","s":"Throwing something useful","x":"throw new Parse.Error(142, \u0027Price must be greater than zero\u0027); A Parse.Error reaches the client as {code, error} with a sensible HTTP status. Anything else becomes a generic 141 wi"},{"t":"Errors","u":"guide/errors.html#validation","s":"Validation errors","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // 142, listing every failure at once } validateOrThrow checks e"},{"t":"Errors","u":"guide/errors.html#conflicts","s":"Write conflicts","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Errors","u":"guide/errors.html#responses","s":"What the client receives","x":"removeResultMiddleware unwraps Parse\u0027s {result: …} envelope on success. Errors pass through unchanged: // success { \"objectId\": \"abc123\", \"name\": \"Widget\" } // failure { \u003c"},{"t":"Errors","u":"guide/errors.html#practice","s":"In practice","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true } }) static async publishArticle(req: Parse.Cloud.FunctionRequest) { const user = req.user!; const [findErr, art"},{"t":"Errors","u":"guide/errors.html#logging","s":"Logging","x":"parse-server logs an uncaught error from a cloud function with the function name, the parameters and the user. Deliberate Parse.Error throws are logged too, so a noisy validation p"},{"t":"Files \u0026 images","u":"guide/files.html","s":"","x":"Uploading, storing and serving files — and the ACL problem that catches everyone the first time."},{"t":"Files \u0026 images","u":"guide/files.html#parsefile","s":"Parse.File","x":"A file is uploaded first and attached second. It has its own lifecycle and its own URL. const file = new Parse.File(\u0027invoice.pdf\u0027, { base64 }, \u0027application/pdf\u0027); await file.save({"},{"t":"Files \u0026 images","u":"guide/files.html#field","s":"Declaring a file field","x":"import { ParseField, implementACL, syncImageAcl, CloudFunction, catchError, BeforeSaveFile, AfterDeleteFile, AfterDelete, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027File\u0027 }) d"},{"t":"Files \u0026 images","u":"guide/files.html#acl","s":"The problem everyone hits","x":"A file\u0027s URL is not protected by the parent record\u0027s ACL. Anyone holding the URL can fetch the bytes. What the ACL protects is the row that points at it. A file URL is effectively "},{"t":"Files \u0026 images","u":"guide/files.html#cascade","s":"Making images follow their parent","x":"When you use a wrapper class, its row has its own ACL — and that ACL does not update itself when the parent\u0027s visibility changes. Publish an article and its cover stays invisible; "},{"t":"Files \u0026 images","u":"guide/files.html#upload-endpoint","s":"An upload endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { base64: { required: true }, fileName: { required: true }, } }, }) static async uploadImage(req: Parse"},{"t":"Files \u0026 images","u":"guide/files.html#triggers","s":"File triggers","x":"@BeforeSaveFile() static async onBeforeSaveFile(req: any) { const { file, user } = req; if (!user) throw new Parse.Error(119, \u0027Sign in to upload\u0027);"},{"t":"Files \u0026 images","u":"guide/files.html#cleanup","s":"Deleting","x":"Deleting a row that points at a file does not delete the file. Storage keeps it until something removes it. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRe"},{"t":"Files \u0026 images","u":"guide/files.html#storage","s":"Where files actually live","x":"AdapterSuits Filesystem (default)Development. Lost on redeploy, not shared between instances S3, Google Cloud StorageProduction GridFSKeeping files in MongoDB — simple, but grows t"},{"t":"Files \u0026 images","u":"guide/files.html#mistakes","s":"Common mistakes","x":"SymptomCause An image is invisible while its record is publicsyncImageAcl not called after the parent\u0027s ACL was set An image stays visible after the record was hiddenSame, on the t"},{"t":"fromParams","u":"guide/from-params.html","s":"","x":"Turns a request body into a typed Parse object, reading your @ParseField declarations to convert pointers, dates and geopoints. It is designed to be pointed straight at raw user in"},{"t":"fromParams","u":"guide/from-params.html#why","s":"The alternative","x":"Without it, every create and update is a run of set() calls that drift from the model each time a field is added: // Twelve lines that must be edited every time the model changes, "},{"t":"fromParams","u":"guide/from-params.html#conversions","s":"What it converts","x":"It walks your declared fields and converts each according to its type. Declared typeSent asBecomes String, Number, Boolean\"Widget\", 12the value, unchanged"},{"t":"fromParams","u":"guide/from-params.html#clearing","s":"Clearing a pointer","x":"Both null and {} mean remove this relationship, so a form that submits an empty select works without special handling: { \"category\": null } // cleared { \"category\": {} } // cleared"},{"t":"fromParams","u":"guide/from-params.html#files","s":"File and image fields are skipped","x":"Pointer fields whose target is an excluded class — IMG and File by default — are deliberately not converted. An uploaded file has to be saved and processed before it can be attache"},{"t":"fromParams","u":"guide/from-params.html#create-update","s":"One call for create and update","x":"// No id in the body -\u003e insert Product.fromParams({ name: \u0027New\u0027 }); // id present -\u003e the same call updates that row Product.fromParams({ id: \u0027prod789\u0027, name: \u0027Edited\u0027 }); That is w"},{"t":"fromParams","u":"guide/from-params.html#safety","s":"Why it is safe on raw input","x":"The obvious worry about mapping a request body onto a model is over-posting: a caller adding {\"isAdmin\": true} and having it stored. That does not happen, and the reason is worth k"},{"t":"fromParams","u":"guide/from-params.html#control","s":"What you do still control","x":"Two things are worth being deliberate about. Neither is a flaw — both are the natural consequence of mapping a body onto a model. Server-controlled fields If a field is declared, a"},{"t":"fromParams","u":"guide/from-params.html#server-fields","s":"Server-controlled fields","x":"If a field is declared, a caller can set it. A status, isApproved or ownerId that your server means to own will be taken from the body if it is there. const product = Product.fromP"},{"t":"fromParams","u":"guide/from-params.html#clp-switch","s":"An id changes which permission applies","x":"Because an id in the body turns the save into an update, Parse checks the update permission rather than create — the row\u0027s ACL write access, and the class\u0027s update CLP. That protec"},{"t":"fromParams","u":"guide/from-params.html#acl-in-body","s":"An ACL sent in the body","x":"A caller can include ACL in the payload. It is not saved — like any undeclared value it stays clean — but getACL() will return it until you set your own. That only matters in one s"},{"t":"fromParams","u":"guide/from-params.html#complete","s":"A complete create endpoint","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { requireUser: true, fields: { name: { required: true } } }, }) static async createProduct(req: Parse.Cloud.FunctionRequest) { const"},{"t":"fromParams","u":"guide/from-params.html#notes","s":"Smaller things","x":"It mutates the params objectSets className on the input so Parse can build from it. Harmless for req.params, surprising if you reuse the same object Absent means untouchedA declare"},{"t":"fromParams","u":"guide/from-params.html#summary","s":"In short","x":"Converts pointers, arrays of pointers, dates and geopoints from your declarations. Never writes a field you did not declare, whatever the caller sends. Skips file and image pointer"},{"t":"Getting started","u":"guide/getting-started.html","s":"","x":"Three commands to a running API with OpenAPI docs, a seeded user, and a MongoDB configured so transactions actually work."},{"t":"Getting started","u":"guide/getting-started.html#requirements","s":"Requirements","x":"Node≥ 20.19 MongoDB≥ 7.0.16 for parse-server 9. A replica set is required for transactions TypeScriptAny version, with experimentalDecorators: t"},{"t":"Getting started","u":"guide/getting-started.html#create","s":"Create a project","x":"npx parse-server-kit new my-api cd my-api npm run db:up npm run dev The generated project prints a ready-to-paste curl on startup, including a session token for "},{"t":"Getting started","u":"guide/getting-started.html#what-you-get","s":"What was generated","x":"my-api/ ├── docker-compose.yml # MongoDB as a replica set ├── tsconfig.json # decorator flags already set ├── .env └── src/ ├── app.ts # boot order — the sequence matters ├── seed."},{"t":"Getting started","u":"guide/getting-started.html#first-request","s":"Your first request","x":"Both of these are real exchanges against a freshly generated project — the responses below are what the server actually returned. Create — needs the session token printed at startu"},{"t":"Getting started","u":"guide/getting-started.html#dashboard","s":"Parse Dashboard","x":"Parse has an official admin console \u0026mdash; browse and edit every class, run queries, inspect users and roles, send push. It is not installed by default, and getting it is one comm"},{"t":"Getting started","u":"guide/getting-started.html#adding","s":"Adding your own entity","x":"psk g resource Product Writes src/models/Product.ts and src/functions/product.ts with the edit points marked, and changes nothing else — there is no module to register into, becaus"},{"t":"Getting started","u":"guide/getting-started.html#tsconfig","s":"TypeScript setup","x":"If you are adding this to an existing project rather than generating one, one compiler flag is mandatory: { \"compilerOptions\": { \"experimentalDecorators\": true } } This library use"},{"t":"Getting started","u":"guide/getting-started.html#parse-docs","s":"Where Parse\u0027s own docs take over","x":"This site covers what this library adds. For Parse Server itself — query operators, ACL semantics, LiveQuery, file adapters, push, authentication providers — the Parse Platform doc"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html","s":"","x":"Clients subscribe to a query and receive updates as rows change — with the same ACL deciding who sees what."},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#what","s":"What it is","x":"A client sends a query and keeps the connection open. When a row that matches is created, changed or removed, the server pushes the event. No polling, no websocket layer to design,"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#enable","s":"Enabling it","x":"Two settings on Parse Server: which classes may be subscribed to, and the websocket server itself. const parseServer = ParseServer({ // … your usual options … liveQuery: { classNam"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#client","s":"Subscribing, from a client","x":"const query = new Parse.Query(Task); query.equalTo(\u0027project\u0027, Project.pointer(projectId)); query.equalTo(\u0027done\u0027, false); const subscription = await query.subscribe(); subscriptio"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#permissions","s":"Permissions","x":"A subscription carries the client\u0027s session token, and every event is checked against the row\u0027s ACL before it is sent. Two users subscribed to the same query receive different even"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#triggers","s":"Server-side hooks","x":"import { ParseClass, BaseModel, BeforeConnect, BeforeSubscribe, AfterEvent, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Task\u0027) export default class Task extends BaseModel { \u003cspan"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#cost","s":"What it costs","x":"Every save on a live class is matched against every open subscription for it. Keep classNames to the classes that genuinely need it. Connections are stateful — they hold memory and"},{"t":"Real-time (LiveQuery)","u":"guide/live-query.html#alternatives","s":"When not to use it","x":"NeedBetter Notify a user who is not on the pagePush notification Refresh a dashboard every minutePolling — simpler and cheaper Stream a high-frequency feedA dedicated channel; Live"},{"t":"Models \u0026 fields","u":"guide/models.html","s":"","x":"One class produces the schema, the indexes, the database validators, the permissions and the OpenAPI definition."},{"t":"Models \u0026 fields","u":"guide/models.html#parseclass","s":"@ParseClass","x":"Registers the Parse subclass and everything derived from it. Applied to a class extending BaseModel. import { ParseClass, BaseModel, ParseField, BeforeSave, validateOrThrow, } from"},{"t":"Models \u0026 fields","u":"guide/models.html#parsefield","s":"@ParseField","x":"Defines a getter and setter on the prototype that read and write Parse\u0027s attribute store. That means product.name is the supported API, not product.get(\u0027name\u0027). @ParseField({ type:"},{"t":"Models \u0026 fields","u":"guide/models.html#field-options","s":"Options","x":"OptionApplies toEffect typeallString, Number, Boolean, Date, Object, Array, GeoPoint, File, Bytes, Polygon, Pointer, Relation requiredallEnforced by validateOrThrow and by the data"},{"t":"Models \u0026 fields","u":"guide/models.html#basemodel","s":"BaseModel","x":"// A reference by id, with no fetch. const ref = Category.pointer(\u0027abc123\u0027); // Build a typed instance from request params, reading @ParseField // metadata to convert pointers, arr"},{"t":"Models \u0026 fields","u":"guide/models.html#validation","s":"Validation","x":"@BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest\u003cProduct\u003e) { validateOrThrow(req.object); // checks every rule declared above } validateOrThrow checks req"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html","s":"","x":"One line on a model, and a stale save is refused instead of silently overwriting."},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#problem","s":"The problem","x":"Two people open the same record, both save. The second write overwrites the first, and neither is told. Guarding against that in every endpoint means remembering to, every time, fo"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#usage","s":"The whole feature","x":"import { ParseClass, BaseModel, ParseVersionField, createVersionedMongoAdapter, VersionRegistry, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Job\u0027) class Job extends BaseModel {"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#requirements","s":"Requirements","x":"Needs createVersionedMongoAdapter, the same adapter that powers transactions. It is MongoDB only; on Postgres the field is declared but nothing enforces it — and since 3.0 that say"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#handling","s":"Recovering","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { const\u003c/span"},{"t":"Optimistic locking","u":"guide/optimistic-locking.html#registry","s":"Inspecting it","x":"VersionRegistry.classNames(); // every versioned class VersionRegistry.isVersioned(\u0027Job\u0027); // true VersionRegistry.adapterIsInstalled(); BackTransactions \u003ca class=\"next\" href=\"seed"},{"t":"Queries","u":"guide/queries.html","s":"","x":"Finding things. Filters, sorting, pagination, pointers, sub-queries and counting — with the permission model doing the filtering you would otherwise write by hand."},{"t":"Queries","u":"guide/queries.html#basics","s":"The shape of a query","x":"Build it, constrain it, run it. Always pass a sessionToken so the ACL applies — see permissions below. import {catchError, ParseClass, CloudFunction} from \u0027parse-server-kit\u0027; const"},{"t":"Queries","u":"guide/queries.html#filters","s":"Filtering","x":"MethodFinds rows where equalTo(k, v)the field equals v notEqualTo(k, v)it does not greaterThan(k, v) / lessThan(k, v)numeric or date comparison"},{"t":"Queries","u":"guide/queries.html#search","s":"Text search","x":"// Case-insensitive substring. Simple, but it cannot use an index - // fine for admin screens, poor for large public listings. query.matches(\u0027name\u0027, new RegExp(search, \u0027i\u0027)); // Pr"},{"t":"Queries","u":"guide/queries.html#combining","s":"Combining conditions","x":"Conditions on one query are combined with AND. For OR, build several queries and join them. const byName = new Parse.Query(Product); byName.matches(\u0027name\u0027, new RegExp(term, \u0027i\u0027)); "},{"t":"Queries","u":"guide/queries.html#pointers","s":"Pointers and related data","x":"A Pointer stores only a reference. Ask for the related row explicitly, or you get {__type, className, objectId} and nothing else. // Fetch the category along with each product quer"},{"t":"Queries","u":"guide/queries.html#sorting","s":"Sorting and pagination","x":"query.descending(\u0027createdAt\u0027); // newest first query.ascending(\u0027name\u0027); // A to Z query.addDescending(\u0027price\u0027); // then by price, within that const\u003c/spa"},{"t":"Queries","u":"guide/queries.html#deep-paging","s":"Paging deeply","x":"skip gets slower the further in you go, because the database still walks the skipped rows. For a large export, page on a sort key instead: let cursor: Date | undefined; while (true"},{"t":"Queries","u":"guide/queries.html#single","s":"Fetching one row","x":"// By id. Throws 101 when missing OR when the ACL hides it. const [err, product] = await catchError(query.get(id, { sessionToken })); // First match, or undefined - no throw const "},{"t":"Queries","u":"guide/queries.html#counting","s":"Counting","x":"const [err, total] = await catchError(query.count({ sessionToken })); // A list endpoint usually wants both, from the same constraints. const rows = await query.find({ sessionToken"},{"t":"Queries","u":"guide/queries.html#permissions","s":"Queries and permissions","x":"This is the part that removes work rather than adding it. // Filtered by the ACL - only rows this user may read come back. await query.find({ sessionToken: user.getSessionToken() }"},{"t":"Queries","u":"guide/queries.html#aggregate","s":"Aggregation and distinct","x":"// Master key required for both. const distinct = await query.distinct(\u0027status\u0027); const pipeline = [ { match: { status: \u0027active\u0027 } }, { group: { objectId: \u0027$category\u0027, total:"},{"t":"Queries","u":"guide/queries.html#patterns","s":"A complete list endpoint","x":"@CloudFunction({ methods: [\u0027GET\u0027], description: \u0027List products\u0027, validation: { fields: { page: { type: String }, limit: { type: String }, status: { type: String }, search: { type: "},{"t":"Queries","u":"guide/queries.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed property access is unavailableThe result was not cast — rows as Product[] A pointer field is {__type: \u0027Pointer\u0027}No include() for it Only 100 rows come backThat"},{"t":"Relationships","u":"guide/relations.html","s":"","x":"Pointer, array of pointers, or Relation — three ways to connect records, and the one question that decides between them."},{"t":"Relationships","u":"guide/relations.html#three","s":"Three options","x":"HoldsGood up toQuery from Pointerone reference—either side Array of pointersa list, stored on the row~100either side Relationa list, stored sep"},{"t":"Relationships","u":"guide/relations.html#pointer","s":"Pointer — one reference","x":"import { ParseField, ParseClass, BaseModel, BeforeSave, cloneAcl, AfterDelete, catchError, } from \u0027parse-server-kit\u0027; @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Category\u0027, requir"},{"t":"Relationships","u":"guide/relations.html#one-to-many","s":"One to many","x":"Put the pointer on the many side. A project does not hold a list of tasks; each task points at its project. // Task @ParseField({ type: \u0027Pointer\u0027, targetClass: \u0027Project\u0027, required:"},{"t":"Relationships","u":"guide/relations.html#array","s":"Array of pointers — a small curated list","x":"// targetClass is what makes fromParams convert the ids @ParseField({ type: \u0027Array\u0027, targetClass: \u0027Tag\u0027 }) declare tags: any[]; { \"tags\":"},{"t":"Relationships","u":"guide/relations.html#relation","s":"Relation — a large list","x":"Stored outside the row, so size does not affect it and adding does not rewrite the parent. @ParseField({ type: \u0027Relation\u0027, targetClass: \u0027User\u0027 }) declare members: any; \u003cspan class="},{"t":"Relationships","u":"guide/relations.html#join","s":"A join model — when the link has its own data","x":"The moment the relationship needs attributes of its own — a role, a joined-at date, an order — it is a record, not a pointer. @ParseClass(\u0027Membership\u0027) export default class Members"},{"t":"Relationships","u":"guide/relations.html#querying","s":"Querying across a relationship","x":"// Tasks whose project is archived - a sub-query const archived = new Parse.Query(Project).equalTo(\u0027status\u0027, \u0027archived\u0027); new Parse.Query(Task).matchesQuery(\u0027project\u0027, archived);"},{"t":"Relationships","u":"guide/relations.html#acl","s":"Permissions across a relationship","x":"A related row has its own ACL. A task is not private because its project is — you have to say so. The usual place is the child\u0027s @BeforeSave: @BeforeSave() static async onBeforeSav"},{"t":"Relationships","u":"guide/relations.html#deleting","s":"Deleting","x":"Parse does not cascade. Deleting a project leaves its tasks pointing at nothing. @AfterDelete() static async onAfterDelete(req: Parse.Cloud.AfterDeleteRequest\u003cProject\u003e) { const que"},{"t":"Relationships","u":"guide/relations.html#summary","s":"Choosing, briefly","x":"RelationshipUse Task → its projectPointer on the task Project → its tasksNo field. Query tasks by project Post → its tagsArray of pointers Team → its members (many)Relation, or a j"},{"t":"Seeding data","u":"guide/seeding.html","s":"","x":"A fresh install has no roles, so every role-gated endpoint refuses everyone — including you. Seeding is what makes an empty database into a working one, and the generated project s"},{"t":"Seeding data","u":"guide/seeding.html#why","s":"Why an empty database is not a working one","x":"Nothing about this is obvious until it happens. You start the server, everything logs green, and the first request fails: The first POST anyone tries POST /api/notes/createNote X-P"},{"t":"Seeding data","u":"guide/seeding.html#two-kinds","s":"Two kinds of seed, and why they are separate","x":"Almost every seeding mistake comes from treating these as one thing: Reference dataSample data What it isroles, the first admin, currencies, countriesdemo users, example rows Witho"},{"t":"Seeding data","u":"guide/seeding.html#idempotent","s":"Write it so you can run it twice","x":"A seed you are afraid to re-run is a seed that drifts out of step with the code, and you find that out during an incident. Every helper in the template finds first and creates only"},{"t":"Seeding data","u":"guide/seeding.html#roles","s":"Role hierarchy: the direction everyone gets backwards","x":"Making an Admin count as an Editor is one line, and it is the reverse of what reads naturally. Adding role A to role B\u0027s roles relation means every member of A is also treated as a"},{"t":"Seeding data","u":"guide/seeding.html#membership","s":"Adding a user to a role is a save on the role","x":"Role membership is a Parse relation, so the write happens on the role, not the user. This trips people up because setting a role field on the user looks like it should work — and i"},{"t":"Seeding data","u":"guide/seeding.html#running","s":"Two ways to run it","x":"At bootnpm run seed How it reaches the databasein process, directlyover REST, to a running server Server must be upno — it is the serveryes Runs"},{"t":"Seeding data","u":"guide/seeding.html#login-gotcha","s":"Why the standalone seed cannot log in","x":"One real quirk, worth knowing before it puzzles you. The template\u0027s sample seed finishes by logging in as the demo user so the startup banner can print a working session token. Run"},{"t":"Seeding data","u":"guide/seeding.html#output","s":"What a run looks like","x":"$ npm run seed Seed complete. roles created Editor, Admin users created admin, demo notes created 2 And immediately again, unchanged: $ npm run seed Seed complete. roles created no"},{"t":"Seeding data","u":"guide/seeding.html#extending","s":"Adding your own","x":"Seed rows are ordinary model instances, so everything the rest of the documentation says applies — including setting an ACL at creation rather than leaving rows world-readable: for"},{"t":"Seeding data","u":"guide/seeding.html#checklist","s":"Before you ship it","x":"Run it twice. The second run must create nothing. Run it against an empty database. It must succeed from nothing. ADMIN_PASSWORD set outside development. Sample data behind the NOD"},{"t":"Transactions","u":"guide/transactions.html","s":"","x":"Everything the body writes lands together, or none of it does — without threading a session through every call."},{"t":"Transactions","u":"guide/transactions.html#setup","s":"Setup","x":"Transactions are powered by a database adapter. Pass it to Parse Server instead of a plain URI: import { createVersionedMongoAdapter, CloudFunction, Transactional, withTransaction,"},{"t":"Transactions","u":"guide/transactions.html#usage","s":"Using it","x":"@CloudFunction({ methods: [\u0027POST\u0027] }) @Transactional() static async placeOrder(req: Parse.Cloud.FunctionRequest) { await order.save(null, { useMasterKey: true }); \u003cspan class=\""},{"t":"Transactions","u":"guide/transactions.html#semantics","s":"What it guarantees","x":"Nested calls join the outer transaction; the outermost caller commits. The body may re-run — up to three attempts on a transient conflict — so it must be safe to repeat. After thre"},{"t":"Transactions","u":"guide/transactions.html#conflicts","s":"Handling a conflict","x":"try { await job.save(null, { useMasterKey: true }); } catch (error) { if (error instanceof Parse.Error \u0026\u0026 error.code === CONFLICT) { // Reload,"},{"t":"Triggers","u":"guide/triggers.html","s":"","x":"All 21 Parse trigger types as decorators, declared next to the thing they act on."},{"t":"Triggers","u":"guide/triggers.html#usage","s":"Declaring a trigger","x":"import { ParseClass, BaseModel, BeforeSave, Cron, CronSchedule, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Product\u0027) export default class Product extends BaseModel { \u003cspan class="},{"t":"Triggers","u":"guide/triggers.html#types","s":"Every trigger","x":"DecoratorRegisters as @BeforeSave @AfterSavebeforeSave / afterSave @BeforeDelete @AfterDeletebeforeDelete / afterDelete \u003cco"},{"t":"Triggers","u":"guide/triggers.html#cron","s":"Scheduled jobs","x":"class Jobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Nightly cleanup\u0027 }) static async cleanup() { … } } CronSchedule covers the usual patterns from EVERY_MINUT"},{"t":"Troubleshooting","u":"guide/troubleshooting.html","s":"","x":"Symptoms that have no error message attached, and where to look for each."},{"t":"Troubleshooting","u":"guide/troubleshooting.html#index","s":"Symptom index","x":"SymptomCauseWhere Decorators do nothing, or behave oddlyexperimentalDecorators not setGetting started — now throws with a clear message No models, empty schema, no routes"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#fixed","s":"Traps that no longer exist","x":"Two were removed rather than documented, because a warning is not a fix: Fields reading undefined A class field could shadow @ParseField\u0027s accessor at target: ES2022 — reads return"},{"t":"Troubleshooting","u":"guide/troubleshooting.html#checklist","s":"When something will not start","x":"Is experimentalDecorators set? Did importFiles print the files it loaded — and are they the compiled ones? Did the registries initialise after Parse Server was mounted? Read the bo"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html","s":"","x":"A checkout API, where getting it wrong costs real money. Pointers and dates decoded straight from the request body, a total the client cannot forge, stock two shoppers cannot both "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#building","s":"What you are building","x":"A storefront back end with four jobs, in order of how badly they go wrong when done naively: JobThe naive versionWhat you will write Decode the request twenty set() calls that drif"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#setup","s":"1. Set up","x":"npx parse-server-kit new shop-api cd shop-api npm run db:up # MongoDB as a replica set - transactions need one npm run dev The replica set matters. Transactions "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#products","s":"2. Products, with stock worth protecting","x":"psk g resource Product Then fill in src/models/Product.ts: import { ParseClass, ParseField, BaseModel, ParseVersionField, roleKey, } from \u0027parse-server-kit\u0027; @ParseClass(\u0027Pr"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#order-model","s":"3. The order model, and every type fromParams decodes","x":"This model exists partly to be realistic and partly to cover, in one place, each field type that fromParams converts for you. @ParseClass(\u0027Order\u0027, { description: \u0027A customer order\u0027"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#placing","s":"4. Placing an order — what the browser actually sends","x":"Here is the whole point of fromParams. The frontend sends flat JSON: ids as strings, dates as strings, a geopoint as a plain object. Your model says what those fields are. fromPara"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#not-fromparams","s":"What fromParams deliberately does not do","x":"It is worth being precise, because the gaps are design decisions rather than omissions: InputResultWhy A key with no @ParseField ignored Only declared fields are set, so an unexpec"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#pricing","s":"5. Never trust the client\u0027s total","x":"The request deliberately did not include a price, and if it had, you would throw it away. Prices come from the database, every time. import { catchError } from \u0027parse-server-kit\u0027; "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#stock","s":"6. Two shoppers, one unit of stock","x":"This is the step that separates a demo from a system. Two requests read stock: 1 at the same moment, both check \"is there enough?\", both say yes, and both write stock: 0. You have "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#conflict","s":"What the loser sees","x":"Two requests, same instant POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"quantity\": 1 }] } POST /api/orders/createOrder { \"lines\": [{ \"product\": \"prd_desk\", \"q"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#permissions","s":"7. Permissions that follow the order\u0027s status","x":"Now the part that a where clause cannot do. The rule is: the customer may always read their own order; they may edit it only while it is pending; staff may always read and write; n"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#transitions","s":"8. Status transitions, gated by role","x":"A customer must not mark their own order shipped. That is not an ACL question — they legitimately have write access while it is pending — it is a rule about which change is allowed"},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#realtime","s":"9. Watching an order in real time","x":"The customer\u0027s page should update itself when the order ships, without polling. Parse\u0027s LiveQuery does that, and — the part that matters — it respects the ACL you already wrote. A "},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#whole-flow","s":"10. The whole flow, on the wire","x":"Start to finish, this is every exchange a checkout makes: 1 · Browse GET /api/products/listProducts?limit=20 Public, no token needed { \u003cspan class=\""},{"t":"Tutorial: orders, stock and money","u":"guide/tutorial-orders.html#built","s":"What you built","x":"ProblemWhat handles itLines you wrote Decoding pointers, dates, geopoints, id arraysOrder.fromParams()1 Forged totalsserver-side pricinga helper Overselling under load"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html","s":"","x":"A small task-tracking API, end to end: models, relationships, permissions that scope data per user, search, and a scheduled job. About twenty minutes."},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#setup","s":"1. Start the project","x":"npx parse-server-kit new tasks-api cd tasks-api npm run db:up npm run dev You now have a running server with a Note example, docs at /api-docs, and a seeded demo"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#model","s":"2. The first model","x":"Delete the example and generate your own: psk g resource Project Open src/models/Project.ts and fill in the fields: import { ParseClass, BaseModel, ParseField, BeforeSave, validate"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#ownership","s":"3. Make projects private to their owner","x":"The CLP above lets any signed-in user reach the class. The ACL decides which rows they see. Set it when the project is created — in src/functions/project.ts: static async createPro"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#relationship","s":"4. A second model, related to the first","x":"psk g resource Task @ParseField({ type: \u0027String\u0027, required: true }) declare title: string; @ParseField({ type: \u0027Boolean\u0027 }) \u003cspan class="},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#inherit-acl","s":"Tasks should follow their project","x":"A task is as private as the project it belongs to. Do that in the trigger, so it holds on every save path: @BeforeSave() static async onBeforeSave(req: Parse.Cloud.BeforeSaveReques"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#listing","s":"5. A list endpoint that filters and paginates","x":"@CloudFunction({ methods: [\u0027GET\u0027], validation: { fields: { project: { type: String }, done: { type: String }, search: { type: String }, page: { type: String }, } }, }) static async"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#cron","s":"6. A scheduled job","x":"Create src/cron.ts and import it from app.ts before the registries initialise: class TaskJobs { @Cron({ schedule: CronSchedule.DAILY_MIDNIGHT, description: \u0027Flag overdue tasks\u0027 }) "},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#try","s":"7. Try it","x":"export APP=tasks-api export TOK=\u003cthe token printed at startup\u003e curl -X POST localhost:1337/api/projects/createProject \\ -H \"X-Parse-Application-Id: $APP\" -H \"X-Parse-Session-Token:"},{"t":"Tutorial: build a real API","u":"guide/tutorial.html#what-next","s":"What you built","x":"Two models with a relationshipschema, indexes and validators derived from them Ten REST endpointsrouted from method names, documented automatically Per-row permissionsenforced by t"},{"t":"Users \u0026 authentication","u":"guide/users.html","s":"","x":"Signup, login, sessions, roles and password reset — the part you would otherwise spend a fortnight building."},{"t":"Users \u0026 authentication","u":"guide/users.html#why","s":"What you get without writing it","x":"Parse ships a _User class, hashed passwords, session tokens with revocation, email verification, password reset, and a _Role system that the ACL layer already understands. None of "},{"t":"Users \u0026 authentication","u":"guide/users.html#model","s":"Extending the user","x":"Add your own fields by declaring a model against the built-in class. import { ParseClass, ParseField, CloudFunction, catchError, invalidateRoles, getUserRoles, getUsersRoles, Befor"},{"t":"Users \u0026 authentication","u":"guide/users.html#signup","s":"Signing up","x":"@CloudFunction({ methods: [\u0027POST\u0027], validation: { fields: { username: { required: true }, password: { required: true }, email: { required: true }, } }, }) static async signUp(req: "},{"t":"Users \u0026 authentication","u":"guide/users.html#login","s":"Logging in","x":"const [err, user] = await catchError( Parse.User.logIn(req.params.username, req.params.password) ); if (err) throw err; // 101 for wrong username or password return { sessionToken:"},{"t":"Users \u0026 authentication","u":"guide/users.html#sessions","s":"Sessions","x":"A session token identifies the caller on every later request. Clients send it as a header: X-Parse-Application-Id: my-api X-Parse-Session-Token: r:8f2c… Inside a cloud function it "},{"t":"Users \u0026 authentication","u":"guide/users.html#roles","s":"Roles","x":"A role is a named set of users, and it is what an ACL and a CLP refer to. // Create once, at setup or in a seed. const acl = new Parse.ACL(); acl.setPublicReadAccess(true); // so A"},{"t":"Users \u0026 authentication","u":"guide/users.html#password-reset","s":"Password reset and email","x":"await Parse.User.requestPasswordReset(email); Parse sends the mail, hosts the reset page and expires the link. It needs an email adapter and a publicServerURL configured on the ser"},{"t":"Users \u0026 authentication","u":"guide/users.html#protecting","s":"Protecting user data","x":"_User rows are readable by default in many setups. Two things to decide deliberately: @ParseClass(\u0027_User\u0027, { clp: { find: { requiresAuthentication: true }, get: { requiresAuthentic"},{"t":"Users \u0026 authentication","u":"guide/users.html#hooks","s":"Reacting to auth events","x":"@BeforeLogin() static async onBeforeLogin(req: any) { if (req.object.get(\u0027suspended\u0027)) { throw new Parse.Error(101, \u0027Account suspended\u0027); } } \u003cs"},{"t":"Users \u0026 authentication","u":"guide/users.html#mistakes","s":"Common mistakes","x":"SymptomCause Typed fields on req.user are unavailableNot cast — req.user! as User A user can edit another userSaved with useMasterKey instead of the caller\u0027s session token Password"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html","s":"","x":"They are not really competitors. One gives you structure and you build the backend; the other is a backend you customise. Here is the honest side-by-side."},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#difference","s":"The actual difference","x":"NestJS is an application framework. It gives you dependency injection, module boundaries and a request lifecycle — and you build authentication, users, roles, permissions, real-tim"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#table","s":"Feature by feature","x":"NestJSparse-server-kit Auth, sessions, password resetyou build itbuilt in Users \u0026 rolesyou build itbuilt in Row"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#side-by-side","s":"The same feature, both ways","x":"\"Users may only see their own orders\" The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable("},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-permissions","s":"\"Users may only see their own orders\"","x":"The clearest difference, because it is the one that never stops costing you. // NestJS - the filter lives in every query, forever @Injectable() export class OrdersService { constru"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-auth","s":"Signup and login","x":"// NestJS: install passport, jwt, bcrypt; write a User entity, an // AuthService, a JwtStrategy, guards, refresh tokens, a password // reset flow with tokens and expiry, and an ema"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#sbs-crud","s":"A CRUD resource","x":"// NestJS: nest g resource orders // orders.module.ts, orders.controller.ts, orders.service.ts, // dto/create-order.dto.ts, dto/update-order.dto.ts, // entities/order.entity.ts + r"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose","s":"When to choose NestJS","x":"Genuinely — these are good reasons, not concessions. You need a database Parse does not serve well. Postgres works, but transactions and optimistic locking here are MongoDB-only. Y"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#choose-parse","s":"When to choose this","x":"The app is mostly records with permissions. Business apps, admin tools, marketplaces, mobile backends — most software, honestly. Per-row authorisation is a real requirement. Multi-"},{"t":"Compared with NestJS","u":"guide/vs-nestjs.html#honest","s":"The honest summary","x":"A fair way to decide Write down what your backend must do. Cross out everything Parse already provides — auth, users, roles, row-level permissions, real-time, files, push, dashboar"},{"t":"Configuration","u":"reference/configuration.html","s":"","x":"Settings, schema generation, indexes and the optional role cache."},{"t":"Configuration","u":"reference/configuration.html#configurekit","s":"configureKit","x":"Optional. Every default reproduces what the library did before these became settings, so a project that never calls it is unaffected. Values resolve when they are used, not at impo"},{"t":"Configuration","u":"reference/configuration.html#allowauthroutes","s":"allowAuthRoutes","x":"restrictRoutes closes Parse\u0027s generic REST API so clients go through the cloud functions you declared. Its auth endpoints are closed along with everything else, and the documented "},{"t":"Configuration","u":"reference/configuration.html#schema","s":"createSchemaConfig","x":"const schema = createSchemaConfig({ adminRole: \u0027Owner\u0027 }); // pass as the `schema` option to ParseServer OptionDefaultEffect adminRolefrom"},{"t":"Configuration","u":"reference/configuration.html#indexes","s":"Indexes","x":"await applyAllIndexes(parseServer); // after listen() await applyMongoValidators(parseServer); Declared asCreates @ParseField({unique: true})"},{"t":"Configuration","u":"reference/configuration.html#role-cache","s":"Role cache","x":"Off by default. requireRoles and getUserRoles() each cost a database round-trip. Role membership rarely changes, so it caches well — but a revoked role keeps working until the entr"},{"t":"Decorators","u":"reference/decorators.html","s":"","x":"Every decorator the package exports, with its signature."},{"t":"Decorators","u":"reference/decorators.html#model","s":"Model","x":"DecoratorTargetPurpose @ParseClass(name, options?)classRegisters the subclass, CLP, ACL template, compound indexes; registers with OpenAPI and flushes pending triggers @ParseField("},{"t":"Decorators","u":"reference/decorators.html#endpoints","s":"Endpoints","x":"@CloudFunction(config)static methodRegisters a cloud function with HTTP methods, validation and roles @ProtectedCloudFunction(config?)static methodAs above, with POST and requireUs"},{"t":"Decorators","u":"reference/decorators.html#triggers","s":"Triggers","x":"All take an optional {description?, validation?}. Object@BeforeSave @AfterSave @BeforeDelete @AfterDelete @BeforeFind @AfterFind Auth@BeforeLogin @AfterLogin @AfterL"},{"t":"Decorators","u":"reference/decorators.html#registries","s":"Registries","x":"Each is initialised once at boot, after Parse Server is mounted. CloudFunctionRegistryinitialize() also builds the route map and reports the versioning setup TriggerRegistryinitial"},{"t":"Decorators","u":"reference/decorators.html#hooks","s":"Registration hooks","x":"For integrations that need to observe registration as it happens. import { onClassRegistered, onFieldRegistered, onFunctionRegistered, } from \u0027parse-server-kit\u0027; onClassRegistered("},{"t":"Middleware","u":"reference/middleware.html","s":"","x":"What each middleware does, and the order they belong in."},{"t":"Middleware","u":"reference/middleware.html#order","s":"Boot order","x":"The sequence is load-bearing. Each step depends on the one before it. import { importFiles, CloudFunctionRegistry, TriggerRegistry, CronRegistry, setupSwagger, } from \u0027parse-server"},{"t":"Middleware","u":"reference/middleware.html#what","s":"What each one does","x":"MiddlewareResponsibility removeResultMiddlewareUnwraps Parse\u0027s {result: …} envelope so responses look like an ordinary REST API validateEntityRoutesResolves /api/{entity}/{action} "},{"t":"Middleware","u":"reference/middleware.html#security","s":"Security notes","x":"The master key is read from the X-Parse-Master-Key header. The request body is still accepted for compatibility and warns once — a secret in a body ends up in every log that record"},{"t":"Utilities","u":"reference/utilities.html","s":"","x":"Helpers, constants and types the package exports."},{"t":"Utilities","u":"reference/utilities.html#catcherror","s":"catchError","x":"The error convention throughout the library — a rejected promise becomes a tuple rather than a try/catch around every await. import { catchError, importFiles, getUserRoles, getUser"},{"t":"Utilities","u":"reference/utilities.html#importfiles","s":"importFiles","x":"importFiles(dir); // .js only importFiles(dir, { extensions: [\u0027.js\u0027, \u0027.ts\u0027] }); // ts-node / tsx importFiles(dir, { verbose: false }); Defaults to .js, the compile"},{"t":"Utilities","u":"reference/utilities.html#roles","s":"Roles","x":"await getUserRoles(user); // string[] await getUsersRoles(users); // Map roleKey(\u0027Editor\u0027); // \u0027role:Editor\u0027, typed as that literal getUsersRole"},{"t":"Utilities","u":"reference/utilities.html#misc","s":"Everything else","x":"syncImageAcl(parent, fields, acl?)Copies a parent\u0027s ACL onto nested image pointers cloneAcl(acl)Deep copy of a Parse.ACL validateObject(object){valid, errors[]} against the declare"},{"t":"Utilities","u":"reference/utilities.html#deprecated","s":"Deprecated","x":"Still exported so existing imports resolve; to be removed in the next major. UserRolesTwo example role names from one project. Declare your own enum and pass it to roleKey, which a"},{"u":"guide/api-docs.html","t":"API documentation","s":"What you get","x":"setupSwagger serves two things. The spec itself is always available at json; the browser UI needs swagger-ui-express, which the generated project installs for you. Call it after the registries have run, or the spec is generated before the endpoints exist and comes back nearly empty."},{"u":"guide/api-docs.html","t":"API documentation","s":"Where the content comes from","x":"Schemas from ParseClass and ParseField. Paths from Route plus the method name. Parameters or request body from validation fields. Security from requiresAuth, requireRoles, requireUser. Summaries, tags and responses from the swagger block on CloudFunction."},{"u":"guide/api-docs.html","t":"API documentation","s":"Tell the page your application id","x":"Every request needs X-Parse-Application-Id and a docs page that does not say which value is a guessing game. A wrong guess comes back as a bare unauthorized that names nothing. Pass appId to setupSwagger and the Authorize dialog shows it. The application id is not a secret."},{"u":"guide/api-docs.html","t":"API documentation","s":"The REST API key is opt-in","x":"Parse only enforces X-Parse-REST-API-Key when restAPIKey is configured, which most deployments do not do. The spec does not require it by default. Pass restApiKey true when you have one."},{"u":"guide/api-docs.html","t":"API documentation","s":"Without swagger-ui-express","x":"The UI package is optional. Without it setupSwagger logs a warning, skips the browser page and still serves the spec at api-docs json. If api-docs returns Cannot GET while api-docs json works, the package is simply not installed."},{"u":"guide/api-docs.html","t":"API documentation","s":"Every option","x":"setupSwagger takes app, config and an optional path that defaults to api-docs. Options are title, version, description, basePath, appId, restApiKey, host and schemes. The spec is always served at the path plus json."},{"u":"guide/api-docs.html","t":"API documentation","s":"Turning it on and off","x":"There is deliberately no enabled option. The way to not have documentation is to not mount it. The generated project reads DOCS_ENABLED and DOCS_PATH from env, so switching docs off is an environment variable rather than a code change, and the banner reports off instead of advertising a dead URL. Docs are on in production by default."},{"u":"guide/queries.html","t":"Queries","s":"Or use paginate","x":"paginate reads limit and skip as the strings a GET sends, caps the limit, and asks the database for the total matching rows in the same round trip via withCount. Returns results, count, limit, skip and hasMore. The hand-written version returns results.length which is the size of the page you already have and no client can paginate with it."},{"u":"guide/from-params.html","t":"fromParams","s":"Server-controlled fields","x":"If a field is declared a caller can set it. That is mass assignment. Declare clientWritable false on the model and fromParams discards the field whatever the request contains. It governs fromParams only, not the field, so your own code writes it freely. The mirror of protectedFields which hides fields on the way out."}] \ No newline at end of file diff --git a/docs/guide/from-params.html b/docs/guide/from-params.html index f01bc1a..9d1c5b6 100644 --- a/docs/guide/from-params.html +++ b/docs/guide/from-params.html @@ -215,14 +215,45 @@

Server-controlled fields

If a field is declared, a caller can set it. A status, isApproved or ownerId that your server means to own will be - taken from the body if it is there. + taken from the body if it is there — with no error, and no log. That is mass + assignment, and it is the reason clientWritable exists. +

+ +

+ Declare it on the model, and fromParams discards the field whatever + the request contains: +

+ +
@ParseField({type: 'String', enum: ['pending', 'approved'], clientWritable: false})
+declare status: string;
+
+@ParseField({type: 'Pointer', targetClass: '_User', clientWritable: false})
+declare owner: Parse.User;
+ +

+ It governs fromParams only, not the field. Your own code writes it as + freely as ever:

const product = Product.fromParams(req.params);
 
-// Overwrite anything the caller must not decide.
-product.set('status', 'pending');
-product.set('owner', req.user!);
+// Ignored if the body sent them; yours to set here. +product.status = 'pending'; +product.owner = req.user!; + +
+ Why declare it rather than overwrite it +

+ Overwriting after the call works, and it is still correct. But it has to be done in + every endpoint that builds this model, and remembered by everyone who adds the next + one. Forgetting is silent — the value is simply the caller's. Declared on the field, + the rule holds wherever fromParams is used. +

+

+ This is the mirror of Parse's protectedFields, which hides fields on the + way out. clientWritable refuses them on the way in. +

+

A @BeforeSave trigger is the stronger place for this, because it applies on diff --git a/docs/guide/queries.html b/docs/guide/queries.html index 1faa033..1b008c8 100644 --- a/docs/guide/queries.html +++ b/docs/guide/queries.html @@ -216,6 +216,42 @@

Sorting and pagination

"page 2" is not guaranteed to exclude what was on page 1.

+

Or use paginate

+

+ The code above is what every list endpoint ends up writing, and the obvious version of + it is wrong in a way that runs. paginate does the whole job: +

+
const query = new Parse.Query(Product).descending('createdAt');
+return paginate<Product>(query, req.params, {useMasterKey: true});
+
+// → {results, count, limit, skip, hasMore}
+

+ It reads limit and skip as the strings a GET sends, caps the + limit, and asks the database for the total matching rows in the same round trip + via withCount(). +

+ +
+ Why count is the total, not the page size +

+ The hand-written version usually returns results.length. That is the size + of the page you already have: a client cannot draw "page 3 of 12" from it, or decide + whether to enable a next button. Nothing fails — the endpoint answers 200 with + plausible JSON, and the list is quietly unusable. +

+

+ A second count() query is the other common answer. It costs two round + trips and can disagree with the first under concurrent writes, which shows up as a + page count that flickers. +

+
+ +

+ paginate deliberately does not sort for you — the right order belongs to the + endpoint. It is still required: an unsorted paginated list repeats and drops rows as + data changes underneath it. +

+

Paging deeply

skip gets slower the further in you go, because the database still walks the skipped rows. For a large export, page on a sort key instead:

let cursor: Date | undefined;
diff --git a/package-lock.json b/package-lock.json
index 907cacc..bb60756 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "parse-server-kit",
-  "version": "3.0.5",
+  "version": "3.1.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "parse-server-kit",
-      "version": "3.0.5",
+      "version": "3.1.0",
       "license": "MIT",
       "bin": {
         "parse-server-kit": "dist/cli/index.js",
diff --git a/package.json b/package.json
index 89ec2fe..7c3d994 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "parse-server-kit",
-  "version": "3.0.5",
+  "version": "3.1.0",
   "description": "Decorator-driven toolkit for Parse Server: models, cloud functions, triggers, cron, REST routing, ACL, transactions, optimistic locking and Swagger, from TypeScript decorators.",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
diff --git a/src/decorators/parseDecorators.ts b/src/decorators/parseDecorators.ts
index 1602a7d..78d1104 100644
--- a/src/decorators/parseDecorators.ts
+++ b/src/decorators/parseDecorators.ts
@@ -69,6 +69,37 @@ interface ParseFieldBaseOptions {
   geo?: boolean;
   /** Create a TTL index that expires documents N seconds after this Date field. Only valid for Date. */
   ttlSeconds?: number;
+  /**
+   * Whether a client may set this field through `Model.fromParams()`.
+   * Defaults to `true`.
+   *
+   * `fromParams` builds an object from request parameters, and by default it
+   * will set **any** field declared on the model. That is convenient and it is
+   * also mass assignment: an endpoint that does
+   *
+   * ```ts
+   * const note = Note.fromParams(req.params);
+   * ```
+   *
+   * accepts `{"title": "…", "status": "published", "views": 9999}` just as
+   * readily as it accepts a title. Nothing is thrown and nothing is logged —
+   * the row simply saves with values the caller was never meant to choose.
+   *
+   * Mark server-owned fields `clientWritable: false` and `fromParams` ignores
+   * them, whatever the request contains. Your own code is unaffected: this
+   * governs one function, not the field. Set it directly when you mean to:
+   *
+   * ```ts
+   * @ParseField({type: 'Number', clientWritable: false}) declare views: number;
+   *
+   * note.views += 1;              // fine — this is your code
+   * Note.fromParams(req.params);  // `views` in the body is discarded
+   * ```
+   *
+   * Parse's own `protectedFields` is the mirror of this and does not replace
+   * it: that hides fields on the way **out**, this refuses them on the way in.
+   */
+  clientWritable?: boolean;
 }
 
 interface ParseFieldWithIndex extends ParseFieldBaseOptions {
@@ -96,7 +127,7 @@ export type ParseFieldOptions =
 export function ParseField(options: ParseFieldOptions) {
   const {
     type, required = false, targetClass, description, indexName,
-    min, max, minLength, maxLength, geo, ttlSeconds,
+    min, max, minLength, maxLength, geo, ttlSeconds, clientWritable,
   } = options;
   const enumValues = 'enum' in options ? options.enum : undefined;
   const pattern = 'pattern' in options ? options.pattern : undefined;
@@ -223,6 +254,8 @@ export function ParseField(options: ParseFieldOptions) {
       ...(pattern !== undefined && {pattern}),
       ...(geo && {geo: true}),
       ...(ttlSeconds !== undefined && {ttlSeconds}),
+      // Stored only when false: absent means writable, which is the default.
+      ...(clientWritable === false && {clientWritable: false}),
     };
 
     existingFields[propertyKey as string] = fieldMeta;
diff --git a/src/global.d.ts b/src/global.d.ts
index 391c32c..557b83a 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -75,7 +75,11 @@ declare namespace Parse {
     select(...keys: string[]): this;
     ascending(...keys: string[]): this;
     descending(...keys: string[]): this;
+    addAscending(...keys: string[]): this;
+    addDescending(...keys: string[]): this;
     limit(n: number): this;
+    /** Return {results, count} from find() instead of an array. */
+    withCount(value?: boolean): this;
     skip(n: number): this;
     find(options?: any): Promise;
     first(options?: any): Promise;
diff --git a/src/index.ts b/src/index.ts
index b844df0..b0cbc08 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -54,6 +54,8 @@ export type { RoleString } from './utils/constants';
 export { catchError, getUserRoles, getUsersRoles, formatCount,
   generateRandomPassword, generateRandomInteger, generateRandomString, sleep } from './utils/helper';
 export { importFiles } from './utils/dynamicImport';
+export { paginate } from './utils/pagination';
+export type { Page, PaginateOptions } from './utils/pagination';
 
 // ── Role cache (opt-in) ──
 export { configureRoleCache, invalidateRoles, roleCacheEnabled, roleCacheStats } from './utils/roleCache';
diff --git a/src/models/BaseModel.ts b/src/models/BaseModel.ts
index 8f35a41..800cc15 100644
--- a/src/models/BaseModel.ts
+++ b/src/models/BaseModel.ts
@@ -38,6 +38,38 @@ function buildPointer(targetClass: string, value: any, field: string): Parse.Obj
   return pointer;
 }
 
+/**
+ * Say — once, and only outside production — that a field was thrown away.
+ *
+ * The discard itself is silent by design. Refusing the request would tell an
+ * attacker exactly which fields are protected, and would break a client that is
+ * merely out of date; neither is worth it, and the value does not land either
+ * way.
+ *
+ * But there is a third caller, and silence serves them badly: the developer who
+ * wired a form to send `status`, watched the request return 200, and cannot see
+ * why the value never arrives. This is for them, which is why it is gated on
+ * NODE_ENV rather than switched off entirely.
+ *
+ * Once per class and field, because a busy dev server would otherwise print it
+ * on every request until the log is useless.
+ */
+const warnedDiscards = new Set();
+
+function warnDiscarded(className: string, field: string): void {
+  if (process.env.NODE_ENV === 'production') return;
+
+  const key = `${className}.${field}`;
+  if (warnedDiscards.has(key)) return;
+  warnedDiscards.add(key);
+
+  console.warn(
+    `[fromParams] Ignored '${field}' from the request body: ${key} is declared ` +
+      'clientWritable: false. Set it in your own code if the server should ' +
+      'choose it. (Development only — this is silent in production.)'
+  );
+}
+
 const ParseObject = Parse.Object as unknown as new (className?: string) => Parse.Object;
 
 export class BaseModel extends ParseObject {
@@ -74,10 +106,29 @@ export class BaseModel extends ParseObject {
     this: T,
     params: any
   ): InstanceType {
+    const fieldsMeta = Reflect.getMetadata('parse:fields', this) || {};
+
+    /*
+     * Strip anything the client is not allowed to set, BEFORE building the
+     * object.
+     *
+     * This has to happen here rather than in the loop below, because
+     * `Parse.Object.fromJSON` assigns every key it is given. Filtering
+     * afterwards would only skip re-setting a value that was already on the
+     * object — which looks like protection and is not.
+     */
+    const source: Record = {};
+    for (const key of Object.keys(params ?? {})) {
+      if (fieldsMeta[key]?.clientWritable === false) {
+        warnDiscarded(this.name, key);
+        continue;
+      }
+      source[key] = params[key];
+    }
     // A copy, so this never writes `className` into the caller's `req.params`.
     // Mutating the request object meant a second call — a retried transaction,
     // say — saw an input the first call had already altered.
-    const source = {...params, className: new this().className};
+    source.className = new this().className;
 
     const raw = Parse.Object.fromJSON(source, true);
     Object.setPrototypeOf(raw, this.prototype);
@@ -85,7 +136,6 @@ export class BaseModel extends ParseObject {
 
     if (params.id) obj.id = params.id;
 
-    const fieldsMeta = Reflect.getMetadata('parse:fields', this) || {};
     const excluded = this.excludedPointerClasses();
 
     for (const field in fieldsMeta) {
@@ -98,6 +148,25 @@ export class BaseModel extends ParseObject {
         continue;
       }
 
+      /*
+       * A field the client is not allowed to choose.
+       *
+       * Without this, every declared field is settable from the request body —
+       * which is mass assignment. `createNote` accepting `{"title": "…"}` also
+       * accepts `{"title": "…", "status": "published", "views": 9999}`, and
+       * nothing objects: the row saves with values the caller was never meant
+       * to pick, no error, no log.
+       *
+       * Silently skipped rather than rejected, deliberately. A client that
+       * sends a field it should not is usually a stale app or a hopeful one,
+       * not something worth failing a request over — and the outcome is the
+       * same either way: the value is not used. Your own code is untouched;
+       * this governs `fromParams`, not the field.
+       */
+      if (fieldMeta.clientWritable === false) {
+        continue;
+      }
+
       // Check exclusion list (per-model override, else the library setting)
       if (targetClass && excluded.includes(targetClass)) {
         continue;
diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts
new file mode 100644
index 0000000..dcad5bc
--- /dev/null
+++ b/src/utils/pagination.ts
@@ -0,0 +1,163 @@
+import {MAX_QUERY_LIMIT} from './constants';
+
+/**
+ * One page of results, and enough to ask for the next.
+ *
+ * `count` is the TOTAL number of rows matching the filter, not the size of
+ * this page. That distinction is the whole point: a client cannot draw
+ * "page 3 of 12", or decide whether to enable a next button, from the length
+ * of an array it already has.
+ */
+export interface Page {
+  results: T[];
+  /** Total rows matching the query, across all pages. */
+  count: number;
+  limit: number;
+  skip: number;
+  hasMore: boolean;
+}
+
+export interface PaginateOptions {
+  /** Page size when the caller does not ask for one. Default 20. */
+  defaultLimit?: number;
+  /** Ceiling on the page size, however large a limit is requested. Default `MAX_QUERY_LIMIT`. */
+  maxLimit?: number;
+  /** Passed through to `find()`. */
+  useMasterKey?: boolean;
+  /** Passed through to `find()`. */
+  sessionToken?: string;
+  /**
+   * Append a `createdAt` tiebreaker to the sort. Default `true`.
+   *
+   * Paging needs a TOTAL order, and "sort your query" is not enough advice:
+   * sorting by a low-cardinality column leaves ties, and ties page exactly as
+   * badly as no sort at all — a hundred rows sharing `status: 'draft'` can
+   * repeat across pages or vanish between them, with nothing reported.
+   *
+   * Appended, never imposed, so it only settles what your own order leaves
+   * undecided.
+   *
+   * Set `false` when you already sort by something unique, or when the extra
+   * key costs you: a sort MongoDB cannot serve from an index is done in memory
+   * and capped at 32 MB, so on a large collection `price` may be indexed while
+   * `price, createdAt` is not. If you turn it off, sort by something unique —
+   * `objectId` works — or accept that pages will not be stable.
+   */
+  tiebreak?: boolean;
+}
+
+/** What arrives in `req.params`. GET values are strings; POST values may not be. */
+interface PageParams {
+  limit?: unknown;
+  skip?: unknown;
+}
+
+function toPositiveInt(value: unknown, fallback: number): number {
+  const n = Number(value);
+  return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
+}
+
+/**
+ * Run a query as one page of a paginated list.
+ *
+ * ```ts
+ * const query = new Parse.Query(Note).descending('createdAt');
+ * return paginate(query, req.params, {useMasterKey: true});
+ * // → {results, count, limit, skip, hasMore}
+ * ```
+ *
+ * This exists because the obvious hand-written version is wrong in a way that
+ * runs perfectly. Returning `results.length` as the count gives the size of the
+ * page you already have — a number no client can paginate with — and nothing
+ * fails: the endpoint answers 200 with plausible JSON. Doing it properly means
+ * `withCount()`, a cast the type definitions do not model, a ceiling on the
+ * limit, and `hasMore` arithmetic, in every list endpoint you write.
+ *
+ * **Sort the query yourself.** Which order a list is in belongs to the
+ * endpoint, and this never chooses it for you.
+ *
+ * That the order is *total* is this function's job, and it is handled: a
+ * `createdAt` tiebreaker is appended to whatever you sorted by, so ties are
+ * settled and pages are stable. Without that, an unsorted list — or one sorted
+ * by a column with repeated values — silently repeats and drops rows as data
+ * changes. Pass `{tiebreak: false}` to opt out; see `PaginateOptions`.
+ *
+ * The limit is capped at `maxLimit`. Without a ceiling, `?limit=999999` is a
+ * denial of service anybody can type into a browser.
+ */
+export async function paginate(
+  query: Parse.Query,
+  params: PageParams = {},
+  options: PaginateOptions = {}
+): Promise> {
+  const {
+    defaultLimit = 20,
+    maxLimit = MAX_QUERY_LIMIT,
+    useMasterKey,
+    sessionToken,
+    tiebreak = true,
+  } = options;
+
+  const limit = Math.min(toPositiveInt(params.limit, defaultLimit) || defaultLimit, maxLimit);
+  const skip = toPositiveInt(params.skip, 0);
+
+  query.limit(limit).skip(skip);
+
+  /*
+   * A tiebreaker, appended rather than imposed.
+   *
+   * Paging needs a TOTAL order. Without one the database is free to return rows
+   * in any order it likes, so a row can appear on two pages or on none as data
+   * changes underneath — and nothing errors.
+   *
+   * "Always sort" is not enough advice, because sorting by a low-cardinality
+   * column does not help: a hundred rows all with status 'draft' tie with each
+   * other, and ties are exactly as unstable as no sort at all. That case is
+   * invisible, and nobody thinks to look for it.
+   *
+   * `addDescending` appends, so it never overrides the caller's order — it only
+   * settles what the caller's order leaves undecided. Choosing WHICH order a
+   * list is in stays the endpoint's business; guaranteeing the order is total
+   * is this function's.
+   */
+  if (tiebreak) query.addDescending('createdAt');
+
+  /*
+   * `withCount()` returns the total alongside the page, in one round trip. The
+   * alternative — a second `count()` query — costs two trips and can disagree
+   * with the first under concurrent writes, which shows up as a page count
+   * that flickers.
+   */
+  query.withCount();
+
+  const findOptions: Record = {};
+  if (useMasterKey) findOptions.useMasterKey = true;
+  if (sessionToken) findOptions.sessionToken = sessionToken;
+
+  /*
+   * The cast is unavoidable. `withCount()` changes what `find()` resolves to —
+   * `{results, count}` rather than an array — and neither @types/parse nor the
+   * SDK's own definitions model that.
+   */
+  const page = (await query.find(findOptions)) as unknown as {
+    results?: unknown[];
+    count?: number;
+  };
+
+  /*
+   * Defensive, because this shape comes from a cast rather than from the type
+   * system. If a future SDK stops honouring `withCount` and returns a plain
+   * array, fall back to it rather than answering with `undefined` results —
+   * the count is then wrong, but the endpoint still returns its rows.
+   */
+  const results = (Array.isArray(page) ? page : (page.results ?? [])) as T[];
+  const count = typeof page.count === 'number' ? page.count : results.length;
+
+  return {
+    results,
+    count,
+    limit,
+    skip,
+    hasMore: skip + results.length < count,
+  };
+}
diff --git a/templates/ai/RULES.md b/templates/ai/RULES.md
index 6b65fda..3737b1b 100644
--- a/templates/ai/RULES.md
+++ b/templates/ai/RULES.md
@@ -88,28 +88,62 @@ const order = Order.fromParams(req.params);
 It does **not** handle nested objects carrying extra data (line items, for
 example) — write that loop explicitly.
 
-### 6. Never let the client decide identity or money
+### 6. Never let the client decide identity, money or status
 
-`fromParams` decodes whatever the body contains, including fields that are
-really the server's business. Overwrite them **after** the call:
+`fromParams` sets every declared field the body carries. A field the server
+owns — a price, a status, an owner, a counter — is therefore settable by anyone
+who can reach the endpoint. Nothing throws; the row just saves with a value
+nobody chose.
+
+Mark those fields on the **model**, so it holds wherever `fromParams` is used:
+
+```ts
+@ParseField({type: 'Number', min: 0, clientWritable: false})
+declare totalCents: number;
+
+@ParseField({type: 'Pointer', targetClass: '_User', clientWritable: false})
+declare customer: Parse.User;
+```
+
+`fromParams` then discards those keys whatever the request contains. Your own
+code is unaffected — the flag governs that one function, not the field:
 
 ```ts
 const order = Order.fromParams(req.params);
-order.customer = req.user!;          // not req.params.customer
-order.totalCents = await priceIt();  // never req.params.totalCents
+order.customer = req.user!;          // your code, still fine
+order.totalCents = await priceIt();
+```
+
+Overwriting after the call also works and is still correct, but it depends on
+remembering, in every endpoint, forever. The declaration does not.
+
+### 7. Paginate with `paginate`, not by hand
+
+```ts
+const query = new Parse.Query(Order).descending('createdAt');
+return paginate(query, req.params, {useMasterKey: true});
+// → {results, count, limit, skip, hasMore}
 ```
 
-### 7. GET parameters arrive as strings
+`count` is the **total matching rows**, not the size of this page. The
+hand-written version that returns `results.length` runs perfectly and gives a
+client a number it cannot paginate with.
+
+**Sort the query.** Pagination over an unsorted query is unstable — MongoDB
+guarantees no order without one, so rows can appear on two pages or none.
+
+### 8. GET parameters arrive as strings
 
 `validateEntityRoutes` merges the query string into the body. Declare them as
 `{type: String}` and convert:
 
 ```ts
-const limit = Number(req.params.limit) || 20;
 const active = req.params.isActive === 'true';
 ```
 
-### 8. Cast query results to the typed model
+`paginate` already reads `limit` and `skip` this way.
+
+### 9. Cast query results to the typed model
 
 `Parse.Query` returns `Parse.Object`, so `row.title` is unavailable until you
 say what it is:
@@ -119,13 +153,13 @@ const rows = await query.find({useMasterKey: true});
 return rows as Product[];
 ```
 
-### 9. Pointer and Relation fields require `targetClass`
+### 10. Pointer and Relation fields require `targetClass`
 
 Without it the decorator throws at import — which is the good case. An **Array**
 field without `targetClass` fails quietly instead: the values stay as the client
 sent them, so a list of ids stays a list of strings that no query will match.
 
-### 10. `@ParseVersionField` declares its own field
+### 11. `@ParseVersionField` declares its own field
 
 No `@ParseField` above it.
 
@@ -136,7 +170,7 @@ declare version: number;
 
 ---
 
-### 11. `requiresAuth` and `rateLimit` are enforced — so mean them
+### 12. `requiresAuth` and `rateLimit` are enforced — so mean them
 
 ```ts
 @CloudFunction({
diff --git a/templates/ai/skills/parse-endpoint.md b/templates/ai/skills/parse-endpoint.md
index 67e3b52..23a8102 100644
--- a/templates/ai/skills/parse-endpoint.md
+++ b/templates/ai/skills/parse-endpoint.md
@@ -61,7 +61,15 @@ export default OrderFunctions;
   `req.params.flag === 'true'`.
 - **Cast query results.** `Parse.Query` returns `Parse.Object`;
   `rows as Product[]` before touching typed properties.
-- **Cap `limit`.** `Math.min(Number(req.params.limit) || 20, MAX_QUERY_LIMIT)`.
+- **Use `paginate` for any list.** It caps the limit, reads the string params a
+  GET sends, and returns the TOTAL rather than the page size:
+  ```ts
+  const query = new Parse.Query(Product).descending('createdAt');
+  return paginate(query, req.params, {useMasterKey: true});
+  // → {results, count, limit, skip, hasMore}
+  ```
+  Sort the query yourself — pagination over an unsorted query repeats and drops
+  rows as data changes, without erroring.
 - **Use `catchError`**, not `try/catch` around `await`.
 - **Set an ACL on anything you create** that is not meant to be world-readable.
   `implementACL` takes a description and **returns** an ACL — it does not take
@@ -70,7 +78,14 @@ export default OrderFunctions;
 ## NEVER
 
 - Never trust the body for identity, price, status or ownership. `fromParams`
-  decodes every declared field, including those. Overwrite them after the call.
+  sets every declared field the request carries, so any of those is settable by
+  the caller. Declare them `clientWritable: false` on the model — that holds
+  wherever `fromParams` is used, rather than depending on every endpoint
+  remembering to overwrite them afterwards.
+- Never hand-roll pagination. `paginate(query, req.params)` returns
+  `{results, count, limit, skip, hasMore}` with `count` as the total, caps the
+  limit, and reads the string params a GET sends. Sort the query first — an
+  unsorted paginated list repeats and drops rows without erroring.
 - Never use `useMasterKey` to make a permission problem go away. It bypasses
   every CLP and ACL. Pass `sessionToken` so the caller's own permissions apply,
   and reach for the master key only when the operation genuinely is the
diff --git a/templates/ai/skills/parse-model.md b/templates/ai/skills/parse-model.md
index 079a045..2d4988e 100644
--- a/templates/ai/skills/parse-model.md
+++ b/templates/ai/skills/parse-model.md
@@ -40,7 +40,9 @@ export default class Product extends BaseModel {
   @ParseField({type: 'String', required: true, unique: true})
   declare sku: string;
 
-  @ParseField({type: 'Number', required: true, min: 0})
+  // Server-owned: `clientWritable: false` makes `fromParams` discard it,
+  // whatever the request body says. Your own code still sets it freely.
+  @ParseField({type: 'Number', required: true, min: 0, clientWritable: false})
   declare priceCents: number;
 
   @BeforeSave()
diff --git a/templates/default/src/functions/note.ts b/templates/default/src/functions/note.ts
index ebf933f..55930de 100644
--- a/templates/default/src/functions/note.ts
+++ b/templates/default/src/functions/note.ts
@@ -1,4 +1,4 @@
-import {CloudFunction, Route, catchError, MAX_QUERY_LIMIT} from 'parse-server-kit';
+import {CloudFunction, Route, catchError, paginate} from 'parse-server-kit';
 import Note from '../models/Note';
 import {Roles} from '../roles';
 
@@ -43,50 +43,28 @@ class NoteFunctions {
   })
   static async listNotes(req: Parse.Cloud.FunctionRequest) {
     const query = new Parse.Query(Note);
-
-    // Cap the page size. Without a ceiling, `?limit=999999` is a denial of
-    // service anyone can type into a browser.
-    const limit = Math.min(Number(req.params.limit) || 20, MAX_QUERY_LIMIT);
-    const skip = Number(req.params.skip) || 0;
-
-    query.limit(limit).skip(skip).descending('createdAt');
     if (req.params.status) query.equalTo('status', req.params.status);
 
     /*
-     * `withCount()` is what makes this pagination rather than a page of rows.
+     * Order it, and order it by something stable.
      *
-     * It returns the TOTAL number of rows matching the filter alongside this
-     * page, in one round trip. The obvious alternative — returning
-     * `results.length` — is the size of the page you already have, which tells
-     * a client nothing: it cannot draw "page 3 of 12", or know whether to
-     * enable the next button. The other alternative, a second `count()` query,
-     * pays for two trips and can disagree with the first under concurrent
-     * writes.
+     * Pagination over an unsorted query is not stable: MongoDB guarantees no
+     * order without a sort, so as rows are written a record can appear on two
+     * pages or on none. Nothing errors; the list is just quietly wrong.
+     * `paginate` deliberately does not choose an order for you, because the
+     * right one belongs to the endpoint.
      */
-    query.withCount();
-
-    const [err, page] = await catchError(query.find({useMasterKey: true}));
-    if (err) throw err;
+    query.descending('createdAt');
 
     /*
-     * The cast is unavoidable: `withCount()` changes what `find()` resolves to
-     * — `{results, count}` instead of an array — and the type definitions do
-     * not model that. Two casts in one line, for two different reasons: the
-     * shape, and the fact that Parse.Query yields Parse.Object rather than the
-     * typed subclass.
+     * `paginate` caps the limit, reads limit/skip as the strings a GET sends,
+     * asks for the TOTAL rather than the page size, and works out `hasMore`.
+     *
+     * The version people write by hand returns `results.length` as the count —
+     * the size of the page you already have, which no client can paginate
+     * with — and it runs perfectly while doing so.
      */
-    const {results, count} = page as unknown as {
-      results: Parse.Object[];
-      count: number;
-    };
-
-    return {
-      results: results as Note[],
-      count,
-      limit,
-      skip,
-      hasMore: skip + results.length < count,
-    };
+    return paginate(query, req.params, {useMasterKey: true});
   }
 
   @CloudFunction({
diff --git a/templates/default/src/models/Note.ts b/templates/default/src/models/Note.ts
index 8f41d1c..3bfd64e 100644
--- a/templates/default/src/models/Note.ts
+++ b/templates/default/src/models/Note.ts
@@ -71,7 +71,15 @@ export default class Note extends BaseModel {
   })
   declare status: string;
 
-  @ParseField({type: 'Number', min: 0})
+  /**
+   * A counter the server owns.
+   *
+   * Without `clientWritable: false`, `fromParams` would accept `{"views":
+   * 9999}` from anyone reaching `createNote` — no error, no log, just a number
+   * nobody chose. The flag governs `fromParams` only; your own code still
+   * writes it freely.
+   */
+  @ParseField({type: 'Number', min: 0, clientWritable: false})
   declare views: number;
 
   @BeforeSave()
diff --git a/test/clientWritable.test.ts b/test/clientWritable.test.ts
new file mode 100644
index 0000000..569c959
--- /dev/null
+++ b/test/clientWritable.test.ts
@@ -0,0 +1,100 @@
+import 'reflect-metadata';
+import {ParseClass, ParseField} from '../src/decorators/parseDecorators';
+import {BaseModel} from '../src/models/BaseModel';
+
+/**
+ * Mass assignment through `fromParams`.
+ *
+ * `fromParams` reads the model's field metadata and sets whatever the request
+ * carried. Every declared field was therefore settable by any caller reaching
+ * an endpoint that used it — including fields the server owns, like a counter
+ * or a status a workflow is supposed to control.
+ *
+ * The failure has no symptom: the request succeeds, the row saves, and the only
+ * evidence is data nobody meant to write.
+ */
+
+@ParseClass('MassAssign')
+class MassAssign extends BaseModel {
+  constructor() {
+    super('MassAssign');
+  }
+
+  @ParseField({type: 'String'})
+  declare title: string;
+
+  @ParseField({type: 'String', clientWritable: false})
+  declare status: string;
+
+  @ParseField({type: 'Number', clientWritable: false})
+  declare views: number;
+}
+
+describe('clientWritable', () => {
+  it('takes the fields a client is allowed to set', () => {
+    const row = MassAssign.fromParams({title: 'Hello'});
+    expect(row.get('title')).toBe('Hello');
+  });
+
+  it('ignores a field marked clientWritable: false', () => {
+    const row = MassAssign.fromParams({title: 'Hello', status: 'published'});
+
+    expect(row.get('title')).toBe('Hello');
+    expect(row.get('status')).toBeUndefined();
+  });
+
+  it('ignores every protected field, not merely the first', () => {
+    const row = MassAssign.fromParams({
+      title: 'Hello',
+      status: 'published',
+      views: 9999,
+    });
+
+    expect(row.get('status')).toBeUndefined();
+    expect(row.get('views')).toBeUndefined();
+  });
+
+  it('does not send a protected field in what save() would write', () => {
+    const row = MassAssign.fromParams({title: 'Hello', views: 9999});
+
+    // The payload is what matters: a field absent here never reaches Parse.
+    const payload = Object.keys(row.toJSON());
+    expect(payload).toContain('title');
+    expect(payload).not.toContain('views');
+  });
+
+  it('leaves your own code free to set the field', () => {
+    // The restriction governs fromParams, not the field. Server code that
+    // means to write it simply does.
+    const row = MassAssign.fromParams({title: 'Hello'});
+    row.set('views', 1);
+
+    expect(row.get('views')).toBe(1);
+    expect(Object.keys(row.toJSON())).toContain('views');
+  });
+
+  it('treats an unmarked field as writable, so nothing existing changes', () => {
+    const row = MassAssign.fromParams({title: 'Still works'});
+    expect(row.get('title')).toBe('Still works');
+  });
+
+  it('does not write `clientWritable: true` into the metadata', () => {
+    // Absent means writable. Storing the default would bloat every field's
+    // metadata and change the schema output for no reason.
+    const fields = Reflect.getMetadata('parse:fields', MassAssign);
+    expect('clientWritable' in fields.title).toBe(false);
+    expect(fields.status.clientWritable).toBe(false);
+  });
+});
+
+describe('the development warning', () => {
+  const env = process.env.NODE_ENV;
+  afterEach(() => { process.env.NODE_ENV = env; jest.restoreAllMocks(); });
+
+  it('is silent in production, so it cannot leak which fields are protected', () => {
+    process.env.NODE_ENV = 'production';
+    const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+    MassAssign.fromParams({title: 'x', status: 'published'});
+    expect(warn).not.toHaveBeenCalled();
+  });
+});
\ No newline at end of file
diff --git a/test/pagination.test.ts b/test/pagination.test.ts
new file mode 100644
index 0000000..43a0933
--- /dev/null
+++ b/test/pagination.test.ts
@@ -0,0 +1,133 @@
+import 'reflect-metadata';
+import {paginate} from '../src/utils/pagination';
+import {MAX_QUERY_LIMIT} from '../src/utils/constants';
+
+/**
+ * `paginate` exists because the hand-written version is wrong in a way that
+ * runs. These pin the parts that are easy to get subtly wrong: the total is the
+ * total and not the page size, the limit has a ceiling, and `hasMore` is right
+ * at the boundary.
+ */
+
+/** A Parse.Query stand-in that records what was asked of it. */
+function fakeQuery(page: unknown) {
+  const calls: Record = {};
+  const q = {
+    limit(n: number) { calls.limit = n; return q; },
+    skip(n: number) { calls.skip = n; return q; },
+    withCount() { calls.withCount = true; return q; },
+    addDescending(k: string) { (calls.tiebreak ??= []); (calls.tiebreak as string[]).push(k); return q; },
+    find(options: unknown) { calls.findOptions = options; return Promise.resolve(page); },
+  };
+  return {q: q as unknown as Parse.Query, calls};
+}
+
+describe('paginate', () => {
+  it('returns the TOTAL, not the size of this page', async () => {
+    // The mistake this replaces: returning results.length, which is 2 here and
+    // tells a client nothing about the other 98 rows.
+    const {q} = fakeQuery({results: [{}, {}], count: 100});
+    const page = await paginate(q, {});
+
+    expect(page.count).toBe(100);
+    expect(page.results).toHaveLength(2);
+  });
+
+  it('asks the query for a count rather than running a second query', async () => {
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {});
+    expect(calls.withCount).toBe(true);
+  });
+
+  it('defaults to 20 per page', async () => {
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {});
+    expect(calls.limit).toBe(20);
+    expect(calls.skip).toBe(0);
+  });
+
+  it('accepts limit and skip as STRINGS, which is how GET sends them', async () => {
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {limit: '5', skip: '10'});
+    expect(calls.limit).toBe(5);
+    expect(calls.skip).toBe(10);
+  });
+
+  it('caps the limit, so ?limit=999999 is not a denial of service', async () => {
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {limit: '999999'});
+    expect(calls.limit).toBe(MAX_QUERY_LIMIT);
+  });
+
+  it('honours a lower maxLimit when one is given', async () => {
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {limit: '500'}, {maxLimit: 50});
+    expect(calls.limit).toBe(50);
+  });
+
+  it('falls back to the default for nonsense input', async () => {
+    for (const bad of ['abc', '', null, undefined, -5]) {
+      const {q, calls} = fakeQuery({results: [], count: 0});
+      await paginate(q, {limit: bad});
+      expect(calls.limit).toBe(20);
+    }
+  });
+
+  it('says there is more when the page does not reach the end', async () => {
+    const {q} = fakeQuery({results: new Array(20).fill({}), count: 100});
+    const page = await paginate(q, {limit: '20', skip: '0'});
+    expect(page.hasMore).toBe(true);
+  });
+
+  it('says there is no more on the exact last page', async () => {
+    // The boundary an off-by-one lives at: 80 + 20 === 100.
+    const {q} = fakeQuery({results: new Array(20).fill({}), count: 100});
+    const page = await paginate(q, {limit: '20', skip: '80'});
+    expect(page.hasMore).toBe(false);
+  });
+
+  it('says there is no more when the result set is empty', async () => {
+    const {q} = fakeQuery({results: [], count: 0});
+    const page = await paginate(q, {});
+    expect(page.hasMore).toBe(false);
+  });
+
+  it('passes useMasterKey and sessionToken through to find', async () => {
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {}, {useMasterKey: true});
+    expect(calls.findOptions).toEqual({useMasterKey: true});
+
+    const b = fakeQuery({results: [], count: 0});
+    await paginate(b.q, {}, {sessionToken: 'r:abc'});
+    expect(b.calls.findOptions).toEqual({sessionToken: 'r:abc'});
+  });
+
+  it('survives an SDK that returns a plain array despite withCount', async () => {
+    // Defensive: the {results, count} shape comes from a cast, not the type
+    // system. If a future SDK stops honouring it, still return the rows.
+    const {q} = fakeQuery([{}, {}]);
+    const page = await paginate(q, {});
+
+    expect(page.results).toHaveLength(2);
+    expect(page.count).toBe(2);
+    expect(page.hasMore).toBe(false);
+  });
+
+  it('appends a tiebreaker so the order is total', async () => {
+    // Paging needs a TOTAL order. A caller who sorts by a low-cardinality
+    // column - status, say - has ties, and ties page exactly as badly as no
+    // sort at all. Appended, so it never overrides the caller's own order.
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {});
+    expect(calls.tiebreak).toEqual(['createdAt']);
+  });
+
+  it('can be told not to, for a caller who knows their indexes', async () => {
+    // The extra sort key is not free: a sort MongoDB cannot serve from an
+    // index runs in memory and is capped at 32MB, so `price` may be indexed
+    // while `price, createdAt` is not.
+    const {q, calls} = fakeQuery({results: [], count: 0});
+    await paginate(q, {}, {tiebreak: false});
+    expect(calls.tiebreak).toBeUndefined();
+  });
+});
\ No newline at end of file