Guide
Seeding data
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 ships with a seed you can read, re-run and extend.
Why an empty database is not a working one
Nothing about this is obvious until it happens. You start the server, everything logs green, and the first request fails:
POST /api/notes/createNote
X-Parse-Application-Id: shop-api
{ "title": "Hello" }
{ "code": 101, "error": "Authentication required" }
{ "code": 119, "error": "Insufficient permissions" }
Both are correct. requireUser and requireRoles are doing
exactly what you asked. There is simply nothing in the database for them to let
through yet.
Two kinds of seed, and why they are separate
Almost every seeding mistake comes from treating these as one thing:
| Reference data | Sample data | |
|---|---|---|
| What it is | roles, the first admin, currencies, countries | demo users, example rows |
| Without it | the app does not function | the app is empty but fine |
| Belongs in production | yes | never |
| In the template | seed() | seedSampleData() |
The generated src/seed.ts keeps them in separate exported functions, and
app.ts only calls the second one when NODE_ENV is not
production. That way "run the seed on deploy" is a safe instruction rather
than a way to put a user called demo with the password
demo-password into your live database.
Write it so you can run it twice
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 if missing:
/**
* Find a role by name, or create it.
*
* The ACL on a role governs who may edit THE ROLE ITSELF - its member list -
* not what its members can do. Public read lets any signed-in request resolve
* role names while evaluating permissions; write is left to the master key
* alone, so nobody can add themselves to Admin through the API.
*/
async function findOrCreateRole(name: string) {
const existing = await new Parse.Query(Parse.Role)
.equalTo('name', name)
.first({ useMasterKey: true });
if (existing) return { role: existing as Parse.Role, created: false };
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(false);
const role = new Parse.Role(name, acl);
await role.save(null, { useMasterKey: true });
return { role, created: true };
}
unique: true, the index refuses the second write, so
the worst case is an error rather than a duplicate. That is why the sample notes in the
template are keyed on slug — the same lookup the database already enforces.
Role hierarchy: the direction everyone gets backwards
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's roles relation means
every member of A is also treated as a member of B. So Admin goes inside
Editor:
async function linkRoleHierarchy() {
const { role: editor } = await findOrCreateRole('Editor');
const { role: admin } = await findOrCreateRole('Admin');
// Already linked? Leave it, so this stays re-runnable.
const linked = await editor.getRoles().query()
.equalTo('objectId', admin.id)
.first({ useMasterKey: true });
if (linked) return;
// Admin inside Editor: an Admin now passes every Editor check.
editor.getRoles().add(admin);
await editor.save(null, { useMasterKey: true });
}
admin.getRoles().add(editor) and nothing throws. You simply find that
your Admin is refused by an Editor-gated endpoint, with no log explaining
why, because from Parse's point of view the permission check ran correctly.
Adding a user to a role is a save on the role
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 it does save, and nothing ever reads it.
// Right: the relation lives on the role.
const { role } = await findOrCreateRole('Editor');
role.getUsers().add(user);
await role.save(null, { useMasterKey: true });
// Wrong, and silent: saves a field nothing consults.
// user.set('role', 'Editor');
// await user.save(null, {useMasterKey: true});
Two ways to run it
| At boot | npm run seed | |
|---|---|---|
| How it reaches the database | in process, directly | over REST, to a running server |
| Server must be up | no — it is the server | yes |
| Runs | unless NODE_ENV=production | whenever you ask |
| Good for | a laptop, a fresh clone | a deploy step, CI |
Both call the same functions. Override the boot behaviour with one variable:
# .env
# Force it either way. Unset, it runs unless NODE_ENV=production.
SEED_ON_BOOT=true
# The first administrator. Without ADMIN_PASSWORD the seed uses a known
# default and the server warns about it on every boot.
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=
ADMIN_PASSWORD is set rather than mentioning it once in a README.
Why the standalone seed cannot log in
One real quirk, worth knowing before it puzzles you. The template's sample seed finishes by logging in as the demo user so the startup banner can print a working session token. Run at boot, that works. Run standalone it does not, and the seed carries on regardless:
import {catchError, implementACL} from 'parse-server-kit';
// It is allowed to fail, and it does fail in one specific case.
//
// Called from app.ts this runs in-process and goes straight to the
// database, so it works. Run standalone it goes over REST - and
// restrictRoutes blocks Parse's built-in /login by design, so it comes
// back 403. The token is a convenience, not part of seeding, so a failure
// here must not fail the seed.
const [loginErr, loggedIn] = await catchError(
Parse.User.logIn(DEMO_USERNAME, DEMO_PASSWORD)
);
if (!loginErr && loggedIn) {
summary.demoSessionToken = loggedIn.getSessionToken();
}
Everything else in the seed uses useMasterKey: true, and the master key
bypasses restrictRoutes entirely — which is why creating roles, users and
rows over REST works while logging in does not. If your clients are Parse's official
SDKs and you need /login open, see
allowAuthRoutes.
What a run looks like
$ 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 none (already present)
users created none (already present)
notes created 0
That second run is the test. If it reports anything created, the seed is not idempotent and it will duplicate on your next deploy.
Adding your own
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 (const sample of samples) {
// `slug` is declared unique on the model, so this lookup is the same one
// the database index enforces - no duplicate is possible even under a race.
const existing = await new Parse.Query(Note)
.equalTo('slug', sample.slug)
.first({ useMasterKey: true });
if (existing) continue;
const note = new Note();
note.title = sample.title;
note.slug = sample.slug;
note.status = sample.status;
// implementACL takes a description and RETURNS an ACL - it does not take
// the object. Published rows are public; drafts are Editors only. Either
// way the author keeps write access.
note.setACL(implementACL({
publicRead: sample.status === 'published',
roleRules: [{ role: 'Editor', read: true, write: true }],
owner: [{ user: demo, read: true, write: true }],
}));
const [err] = await catchError(note.save(null, { useMasterKey: true }));
if (err) throw err;
}
The result, checked over the wire:
GET /api/notes/listNotes
{ "results": [
{ "objectId": "MJq00knE1O", "title": "Welcome",
"slug": "welcome", "status": "published",
"ACL": { "*": { "read": true },
"role:Editor": { "read": true, "write": true } } }
], "count": 1 }
The draft exists and is simply invisible — the ACL, not a where clause,
decided that. See Permissions & ACL.
Before you ship it
- Run it twice. The second run must create nothing.
- Run it against an empty database. It must succeed from nothing.
ADMIN_PASSWORDset outside development.- Sample data behind the
NODE_ENVcheck, or deleted once you have real data. - Everything using
useMasterKey: true— a seed runs as the system, not as a user.
Next: Permissions & ACL for what the roles you just created actually gate, and Users & auth for signing the rest of them up.