skiffr

Seeding

Fill a preview or a local database with a realistic account to click through.

SEED decides what the api puts in an empty database when it starts.

SEEDWhat you get
nonenothing
demothe template's sample notes and users, in an empty database only
previewthe whole product filled in, described below. Refused in production

The preview dataset

SEED=preview creates three accounts that share one password and fills the first one's account with a believable job search. Everything is generated from a fixed seed and dated relative to now, so the charts always end today and every run looks the same.

AccountEmailWhat it is
demodemo@skiffr.testthe account with all the data
adminadmin@skiffr.testan admin, with the users page filled by a dozen sample users
secondsecond@skiffr.testa small account of its own, for checking that nobody sees anyone else's data. It even saved one of the same postings

The password is SEED_DEMO_PASSWORD, or skiffr-preview-demo when it is not set. The default is public, and so is the extension token printed in the links json (both derive from the repo): anyone who can reach a preview can sign in with them. That is fine for throwaway data, which is why the seed refuses to run where real people could sign in. An empty SEED_DEMO_PASSWORD= falls back to the public default.

What the demo account holds:

  • Profile: name, contact, location, work authorization, links, salary expectation, plus experience, education, skills, languages and notice period as extra fields.
  • Answers: 65 answers in every status (learned, confirmed, rejected) from every source, with usage and correction counters, fingerprints and embeddings. Three of them show up under "Needs your call": one to keep, one that keeps changing and one that gets cleared.
  • Site profiles: saved forms for the playground host and for invented boards on Greenhouse, Lever, Ashby and Workable, so autofill fills end to end on a page that asks the standard questions.
  • Jobs: 89 across queued, applied and skipped, from the extension, by hand, Gmail (LinkedIn, Indeed, Glassdoor, other) and three watched sources. The queue starts with a few stale jobs and has whole import batches sharing one timestamp, so "Open next" and the ordering have something to do. The queue also holds three twin postings under different sources, a very long title, a unicode company and one with a hostile title (bidi and zero-width characters, markup) to check escaping.
  • Applications: 40 over ten weeks (the stats page opens on 30 days, which shows 21 of them and about 24% replies; pick 90 days for all 40 and about 45%) with a weekly shape, replies, interviews, offers and rejections, match scores that rise with how far an application went, and queue-to-apply times from hours to weeks.
  • Autofill history: about 500 filled fields tied to those applications, with corrections getting rarer over time. That holds across all 40; the answers page only compares the last 8 applications, which are few enough that its sentence can read either way.
  • Sources: three healthy watched sources, one failing with backoff state, one paused and one never checked.
  • Gmail: a mailbox in the needs_reconnect state with a placeholder in place of a token (nothing ever calls Google) and the mails it "read", linked to the jobs they added.
  • CV: a connected Reactive Resume with a base resume, when CV_ENCRYPTION_KEY is set. The stored key only works against the mock Reactive Resume (@skiffr/orpc/cv-test); point SEED_CV_URL at wherever that mock runs, it defaults to https://rr.test.
  • Extension tokens: one active (its secret is in the links json, so the extension can connect), one active that was used long ago, one revoked and one expired.
  • Notes.

Without an OPENAI_API_KEY the api uses a deterministic stand-in for embeddings when SEED=preview: questions that share their meaningful words (after stopwords, plurals and a few synonyms like salary and compensation) are near each other, so similar-question suggestions can be clicked through. It is not a model and says nothing about how real embeddings would behave. With a real OPENAI_API_KEY the api uses real embeddings, but the seeded answers still hold the stand-in vectors, so suggestions between seeded and new answers compare unrelated spaces.

Running it

On start, set SEED=preview (and SEED_RESET=on to wipe and reseed on every start). Or run it by hand:

pnpm --filter api seed:preview             # seeds once, prints the links json
pnpm --filter api seed:preview --reset     # wipes the preview users and their data first, dates move to now
pnpm --filter api seed:preview --markdown  # prints a PR comment instead of json
pnpm --filter api seed:preview --out seed-links.json

A rerun without --reset changes nothing except resetting the three passwords to the current one, so rows a later version of the seed adds only show up after a reset. --reset (or SEED_RESET=on) removes the three accounts, the sample users and everything that belongs to them, found by their ids: any other user is left alone. If a real account already uses the email of a seed or sample user, the seed stops with an error before changing anything, with or without reset.

KeyWhat it does
SEED_DEMO_PASSWORDthe shared password, at least 10 characters
SEED_RESETon wipes and reseeds on every start
SEED_LINKS_PATHwhere to write the links json on start
PLAYGROUND_URLqueued jobs, their site profiles and the watched sources point at it. Unset, they use example.invalid hosts that never resolve
SEED_CV_URLthe Reactive Resume the seeded connection points at
SEED_ALLOW_PRODUCTION_BUILDyes allows a production build with DEPLOY_ENV=local, for a throwaway docker run

Queued playground jobs look like <PLAYGROUND_URL>/<ats>/<company>/jobs/<number>, and the standard form the saved site profiles describe is STANDARD_FORM in apps/api/src/seed/fixtures.ts.

Where it runs

The preview seed creates accounts with a known password, so it only runs where nothing real lives. It is an allowlist on the exact value of DEPLOY_ENV: pr-<n> (a positive number, lowercase), or local when NODE_ENV is development or test. Everything else is refused, whatever NODE_ENV or SEED_ALLOW_PRODUCTION_BUILD say: production, Production, prod, staging, pr-, PR-1, a value with a space or a newline. DEPLOY_ENV defaults to local, so a self-hosted api started without it counts as local: the database check below is what protects it. A production build (NODE_ENV=production) with DEPLOY_ENV=local needs SEED_ALLOW_PRODUCTION_BUILD=yes.

Outside pr-<n> previews the seed also looks at the database and stops when it holds any user that is not a preview or sample user (the ADMIN_EMAIL account is fine). A rerun that changes nothing is not blocked. In previews testers sign up, so that check is off there.

A refusal stops the api (or the command) with a clear message. The command checks before it opens the database, so a refused run does not migrate anything. When SEED is not preview the seed code is never loaded.

Two things to know about previews: the password and token are public, so anyone who finds the preview url can use the demo accounts, and a preview with an AI provider key lets them spend the preview's AI budget. Set AI_DAILY_CALLS (and AI_DAILY_TOKENS, AI_GLOBAL_DAILY_CALLS) on previews so that spend has a cap.

The command and the api (with SEED_LINKS_PATH) write what the preview tooling needs to tell people where to click:

{
  "users": { "demo": { "email": "demo@skiffr.test", "password": "…", "role": "user" }, "admin": {}, "second": {} },
  "extensionToken": "skp_…",
  "dashboard": { "signIn": "…", "queue": "…", "stats": "…", "answers": "…", "sources": "…", "cv": "…", "gmail": "…" },
  "playground": { "base": "…", "nextJob": "…", "jobs": ["…"] },
  "dedupe": [{ "url": "…", "matches": "…" }],
  "escaping": { "title": "…", "company": "…" }
}

playground is null without PLAYGROUND_URL. Adding any dedupe url as a job returns the existing job instead of a new row. escaping is the queued job to look at when checking that titles are rendered as text.

Adding to the seed

When a feature adds a state worth seeing, add it to the seed (apps/api/src/seed) and to preview.test.ts so every preview shows it. The seed is only for fake data: no real names, addresses, numbers or links. Profile links use example.invalid. The job boards keep their real hosts (greenhouse.io, lever.co, ashbyhq.com, workable.com, linkedin.com, indeed.com, glassdoor.com) because the ats detection and the dashboard icons key on them, but every board slug starts with skiffr-demo- and the LinkedIn and Glassdoor ids are out of range, so no link should point at a real posting. Company names are invented; a resemblance to a real one is a coincidence.

On this page