All pages

Getting started

Data and privacy

What Krinoa stores about a candidate, what leaves for a model provider, and the export, erasure and retention verbs — with the gaps stated plainly.

What is stored#

WhereWhat it holds
CandidateName, email, phone as typed plus its E.164 form, location, LinkedIn and GitHub URLs, timezone, the résumé, its extracted text, a parsed profile (headline, roles, education, skills), WhatsApp consent facts (opt-in and opt-out times, the source of consent), created and updated timestamps
ApplicationIts public id (A-142), stage, source, cover letter, answers to the job's questions, rejection reason, the spam flag and its reason, the current read and its trigger, thread state (reply routing token, last inbound and outbound times, the thread's owner), applied date
ReadsOne record per read: the model id, rubric version, status, latency, the inputs the model was given (job text, questions and verbatim answers, cover letter, the résumé) and the read it produced
MessagesEvery email and WhatsApp message either way: direction, status, subject, body, attachments, and the raw .eml of inbound mail
InterviewsRounds, proposed and booked times, timezone, RSVP state, outcome, and the candidate's scheduling token
Team notesScorecards, comments and prep notes about the candidate — the team's own material, not the candidate's
Activity ledgerOne entry per event: kind, actor, timestamps, references to the job, application and candidate, and a small payload
QuarantineInbound messages that could not be matched to an application, with their addresses, text and raw payload

Two things are not stored with an application: the applicant's IP address, which is hashed before it is used as a rate-limit key and is not written to the application, and any account for the candidate — applying creates no login. A candidate may separately create a Krinoa account to track the applications they have sent; it is an account with Krinoa, not with your workspace, and nothing about it is visible to you. See The candidate's page below.

A résumé is accepted as PDF or DOCX up to 5 MB; text extraction reads PDFs only, and a PDF's text layer is extracted once. Applying again with a new file replaces the old one, and the superseded file is deleted. GET /api/applications/{ref}/resume is the only way to the bytes, and there is no permanent public URL: the route runs the same visibility check as the packet, so an interviewer who is not assigned to the application gets the packet's own 404, and every access writes a resume.viewed ledger entry — at most one per person, per application, per 24 hours.

What leaves for a model provider#

With no screening model configured, nothing leaves for a model at all: the product works unscored, and the AI-use notice is not shown. Screening covers choosing one.

With a model configured, each read sends the provider:

  • The job's title, description and requirements.
  • Every custom question and the candidate's verbatim answer.
  • The cover letter.
  • The résumé — normally as a PDF file part, otherwise as its extracted text capped at 100,000 characters.
  • On a re-read, the previous read plus whatever new evidence triggered it, including candidate replies capped at 4,000 characters each.

The whole prompt is capped at 150,000 characters, and what was cut, what was sent and what came back are all recorded with the read. One additional call per new résumé parses the candidate profile from the same material.

Three other services see candidate data, each only if you connect it:

ServiceWhat it receivesWhen
Your email providerThe candidate's address and the message bodyEvery email your workspace sends or receives
Meta (WhatsApp Cloud API)The candidate's phone number and the messageOnly with a connected number and the candidate's opt-in
Cloudflare TurnstileThe challenge token and the client IPOnly when the captcha is enabled; the browser loads the challenge script on the apply form and on the sign-in screens that send mail

The apply form tells the candidate that AI helps review applications and that a person makes every decision, and the confirmation email repeats it. Every confirmation email — with or without a model configured — also ends with "To see or delete what you've shared, reply to this email" and a link that signs them in to that application on their own page; all of it is appended by the product, so it cannot be edited away. Every apply form also says: "You can ask {Org} to see or delete what you've shared, at any time."

The candidate's page#

A candidate can create a Krinoa account at /me and see the applications they have sent — to your workspace and to every other workspace on Krinoa. The account belongs to the candidate, not to any workspace: there is no password, only a six-digit code mailed to the address, and verifying that code is both the sign-up and the sign-in.

The match is the email address, and only the email address. A card appears on somebody's page when a workspace holds a candidate record whose address equals the one they verified. Nothing links an account to a candidate record: erase a person and their card disappears; correct a person's address and the record follows whoever can prove the new one.

Per application, a candidate sees the workspace's name and logo, the job title, the date they applied, one status word, any interview still waiting for them with the same scheduling link they were mailed, their résumé, which opens in the browser, and a read-only copy of what they submitted: the form's fields and their answers to your questions, as the job's form labels them now. A referrer's name is never shown. The status is one of eight words, and none of them is one of your stage names:

They seeWhen
ReceivedApplied
In reviewScreening — and a rejection you chose not to email
InterviewInterview
OfferOffer
HiredHired
Not moving forwardRejected and the decision email reached sent or delivered
WithdrawnThe application is in the withdrawn stage
Position closedThe job is closed and their application was still open (or was silently rejected)

After 14 days with nothing visible changing — no stage move, no message from you — a received or in-review card adds one line saying so. Fourteen days is fixed in this version, not a workspace setting.

What the page does not show, at any status: your stage names, the read and its score, summary or findings, the spam flag or its reason, the rejection reason, who is assigned, thread ownership, comments, scorecards, prep notes — or anything at all about another workspace. No workspace can see another's card, and no count of a candidate's other applications reaches a recruiter.

The page is read-only. A candidate cannot edit their details, replace their résumé, withdraw, or export or delete their data from it. A request to do any of those reaches you as a reply to the confirmation email, which lands in the Inbox as needs-reply on their thread; you serve it with the export and erasure verbs.

Deleting the account is not deleting the applications. Closing a Krinoa account removes the account and its profile and nothing else; every workspace keeps its own copy of every application, under its own retention window. The reverse also holds: retention erasing a terminal application makes its card disappear from the candidate's page, because the application it described is gone.

Product analytics#

Product analytics is off until you set NEXT_PUBLIC_POSTHOG_KEY. Unset — the default — no analytics script is loaded in any browser, nothing leaves the server, and what breaks is written to Krinoa's own server log and stops there.

Set, the key sends to PostHog, and what is sent depends on which surface the page is:

WhereWhat is sent
The signed-in app and every door into it — sign-in, sign-up, the device-code page, an invitation or verification link, a password resetPageviews, browser and server errors, and one event per activity-ledger entry — its kind, the workspace, and the ids of the job, application or candidate it refers to. The teammate is identified by their user id, name and work address, and grouped by workspace (its name and slug)
The landing page and these docsAnonymous pageviews and errors, held in memory for the tab. No cookie, nobody identified
Every candidate-facing surface — a careers page, an apply form, the embed, a scheduling link, the public APIIn the browser, nothing: the script is not loaded at all. On the server, what an application causes is still a ledger entry, so its event (activity:application.received, with the application's and the candidate's internal ids) is sent like any other, and a server error on one of these pages is reported with the page's path — a scheduling or referral token and any query string removed

Two rules hold wherever it does run. Autocapture is off, so no clicked element, typed value or line of page text is ever collected — the screens that show candidate data send only the fact that they were opened — and session recording is off with it. And an event's properties are ids: a candidate's name, address, phone number, résumé or message body is not a property of anything, on any tier.

Logs. With the same key, the server also sends its own log lines to PostHog: every line written at info or above, or at LOG_LEVEL when that is set higher. A log line carries a scope, a message that is the same every time, the ids of what it was about, and counts and codes — and, for an error, the error, with a failed query's values and any email address removed. Free-text fields never go: a field that is not an id, a count or a code arrives as [redacted], and a stored file's key loses the name of the file that was uploaded. A line is joined to the teammate's user id on a signed-in request, and to the browser's analytics id and session when the browser sent them — which only the app and its doors, the landing page and these docs do. Candidate-facing pages load no analytics script, so their requests never carry them.

The browser talks to /ingest on your own origin, which the app proxies to PostHog, so a blocked third-party domain changes nothing about what is or is not collected.

Traffic, for now. Until PostHog is configured on the hosted product, Vercel Web Analytics counts pageviews on the same two tiers — the app and its doors, the landing and the docs — and on nothing a candidate opens. It is cookieless and identifies nobody: a page path, a referrer, a country and a browser family, aggregated. It needs no key; on an instance that is not on Vercel it is inert. It is a stopgap and goes when PostHog is live.

Export#

candidates:export is an owner and admin capability, works from an API key, and writes a candidate.exported ledger entry. A candidate who asks for their data gets it this way; their own page does not offer it.

sh
krinoa candidates export 3f1c0d2e-0000-4000-8000-abcdef012345 --out ./dsar

Without --out the command prints the document; with it, the directory gets candidate-<id>.json and the résumés it references. The same document is at GET /api/v1/candidates/{id}/export, and it holds:

  • Per candidate — their fields and WhatsApp consent facts.
  • Per application — the job, stage, source, dates, answers, cover letter, rejection reason, the current read, every message on either channel, every interview, the stage history, and a path into the access-checked résumé route rather than a public link.
  • Not included — scorecards, comments, prep notes and internal assignees: the team's working notes about a decision, not the subject's data.

Erasure#

candidates:erase is an owner and admin capability, works from an API key, and is irreversible; the CLI refuses without --yes and prints what would be erased first.

sh
krinoa candidates erase 3f1c0d2e-0000-4000-8000-abcdef012345 --yes

The same verb is DELETE /api/v1/candidates/{id}, and the packet's "…" menu has Erase this person… behind a confirmation that names them and counts their applications and files. It deletes the candidate and cascades their applications, assignments, interviews, scorecards, comments, notifications and messages; redacts quarantined mail from their address or number; and removes every stored file those rows pointed at — résumés, attachments, raw .eml archives and WhatsApp media. File deletion is durable rather than best-effort: every key is recorded in the same transaction as the record deletes and retried until the bytes are gone. Two narrower verbs use the same machinery — krinoa apps delete A-142 --yes deletes one application and its files, taking the candidate with it when it was their only one, and deleting a job erases the candidates whose only application was to that job.

Nothing that carries the person's words, name, address or number survives. Three anonymized records remain:

  • The activity ledger keeps each entry's kind, actor and timestamp, nulling the references and rewriting every payload key that can carry personal data to [erased]. One candidate.erased entry records counts and no name.
  • A read keeps its model id, rubric version, status, latency, timestamp and score and is stamped redacted, with the job text, questions and answers, cover letter and the model's prose nulled. Opening it answers "This read was redacted".
  • A quarantined message keeps only its message id, reason and resolution, so a redelivery of the same message dedupes instead of re-creating the person.

Retention#

Retention is one setting per workspace, at Settings › Data › Retention, and it is off by default: with it unset, nothing is deleted automatically.

sh
krinoa org retention set --months 24krinoa org retention set --months none

The window is a whole number of months from 1 to 120, or none. When it is set, a scheduled cleanup erases candidates who meet all of these: every one of their applications is terminal (rejected, hired or withdrawn); their last touch — the most recent of their application date, the last message either way, and the last interview's end — is older than the window; and nobody in the workspace has a live application for them. Those erasures take the same path as a manual one, ledgered with reason: "retention" and no actor.

The same cleanup clears five other things on fixed windows, whatever the retention setting says:

WhatWindow
A read's candidate text — job text, questions and answers, cover letter, the model's proseRedacted at four years; the anonymized record is kept indefinitely, the candidate's words for four years at most
Resolved quarantine rows30 days
Read notifications90 days
Interview scheduling tokens30 days past the interview
Expired invitations, sessions and verification rowsTheir own expiry

What is not built#

A candidate's request to see, correct or delete their data arrives as a reply to the confirmation email and lands in the Inbox as needs-reply on that candidate's thread; the team serves it with krinoa candidates export and krinoa candidates erase, or the same two verbs from the packet. Both work from an API key and both write their own ledger row, so a request handled by an agent is as auditable as one handled by a person — see the CLI. The gaps, stated plainly:

  • No per-workspace privacy policy URL. The careers page cannot link to your own policy. If you need one in front of candidates, put it in the job description or the About text.
  • No workspace deletion and no user account deletion verb. Comments and scorecards already survive a missing author, rendering as "a former teammate".
  • Retention is one window per workspace. There is no per-job override.
  • No way to record a withdrawal. The candidate's page no longer withdraws, and the team cannot move an application into withdrawn. A candidate who asks to withdraw stays in whatever stage you leave them in.

Updated 2026-09-26