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:

The first POST anyone tries
POST /api/notes/createNote
X-Parse-Application-Id: shop-api

{ "title": "Hello" }
Because there are no users
{ "code": 101, "error": "Authentication required" }
And once you have a user, because there are no roles
{ "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 dataSample data
What it isroles, the first admin, currencies, countriesdemo users, example rows
Without itthe app does not functionthe app is empty but fine
Belongs in productionyesnever
In the templateseed()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 };
}
Let a unique index do the work where you can Find-or-create has a race: two seeds running at once can both miss and both create. Where the field is declared 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 });
}
Backwards fails silently Write 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 bootnpm run seed
How it reaches the databasein process, directlyover REST, to a running server
Server must be upno — it is the serveryes
Runsunless NODE_ENV=productionwhenever you ask
Good fora laptop, a fresh clonea 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=
Change the admin password before it leaves your machine The fallback exists so a fresh clone runs with no configuration. That is right on a laptop and wrong everywhere else, which is why the server prints a warning on every boot until 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:

Anonymous request
GET /api/notes/listNotes
Only the published one comes back
{ "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

Next: Permissions & ACL for what the roles you just created actually gate, and Users & auth for signing the rest of them up.