SourcingNavSecurity
Security at SourcingNav: what is in place, and what is not
Read from the code and its configuration on 16 October 2026
Every statement on this page is read from SourcingNav's code or its deployment configuration, and a test fails when the numbers on it stop matching the code. Where something is not in place, the page says so.
Before anything else: SourcingNav has no SOC 2 report and no ISO 27001 certificate, and no penetration test report is published here. What follows is what the software does.
Where it runs
- Hosting. The site and the API run on Vercel. The API is one Python function in Vercel's
pdx1 region (Portland, Oregon, United States). The build copies the pages into their own output folder and never copies the API's source into it, so only the function can answer an API address.
- Database. Turso (libSQL), reached over HTTPS with a token. Writes that must happen together are sent as one transaction.
- Scheduled jobs. Vercel runs them; each job's address refuses a caller without the cron secret.
- Code, and the jobs that write to the database from GitHub. GitHub holds the source and runs the test gate on every change. Four GitHub Actions workflows hold the database credentials, as secrets of a GitHub environment (an Actions variable of the same name is accepted as a fallback): the two scheduled loads (the FAA airman register and the BLS wage survey), the taxonomy re-seed that runs on every taxonomy change merged to main, and the demo desk seed, run by hand.
- Every outside service is on the subprocessor list, with what it receives.
In transit
- The site is served over HTTPS.
- Every call the server makes to a service on the subprocessor list is over HTTPS; a recruiter's own Gmail is reached over SMTP and IMAP on their TLS ports (465 and 993). A public page someone points the server at is fetched at the address given.
Signing in
- Email link. A sign-in link is signed with a server secret and expires after 15 minutes. With no secret configured, no link is issued and none is accepted. Requests are limited to 5 per address and 20 per network address an hour.
- Sessions. A session lasts 90 days and is renewed while it is in use (when fewer than 75 days remain). The browser holds it and sends it in the
Authorization header; no session cookie is set. The server keeps only a SHA-256 hash of it, and a session is accepted only while its row exists and is not revoked. Signing out revokes one session; "sign out everywhere" revokes them all.
- Google or Microsoft. The provider vouches for one address and it signs in exactly as an email link to it would. The ID token's audience, issuer and expiry are checked. Google is trusted only for a verified Gmail address or a Workspace account on its own domain; Microsoft only for a personal account on Microsoft's own domains or a tenant that proved it owns the domain, never a guest. The sign-in is bound to the browser that started it, the hand-off is good for two minutes and once, and no address bar ever carries a session.
- A suspended account cannot sign in, and the desk a former team member leaves behind never signs in at all.
API keys
- A key (it starts
snk_) is shown once when it is made; the server keeps only its SHA-256 hash. Keys can be revoked at any time.
- A key can be made only from a signed-in session; a key can never make another key.
- A read key can read, and can call a fixed list of draft-and-record actions (search, a like or pass, drafting a message, adding someone to the bench or a role) and the paid contact lookups behind the extension's Find buttons, so a leaked read key can spend lookup credit up to the hourly ceiling. It can never send email, delete anything or change settings.
- A send key adds exactly one action,
/api/outreach/send, and is made only when the recruiter ticks the box that lets the extension send email they approve. Every rule of a send (opt-out, the address check, the daily cap) still applies.
Secrets and stored credentials
- Provider keys and signing secrets are read from environment variables: the project settings on Vercel for the API, a GitHub environment's secrets for the workflows that write to the database. Production refuses to start without its AI key.
- A recruiter's Gmail app password, Gmail connection token and Outlook connection token are stored encrypted (Fernet: AES-128 with an HMAC-SHA256 check) under a key held in the environment. Without that key, those connections refuse to be saved.
- Session tokens and API keys are stored only as hashes.
Not in place: the Google Calendar connection's refresh token is stored as Google issued it, without that application-level encryption.
Signed links and webhooks
- The links in SourcingNav's emails (unsubscribe, scheduling, a privacy-request confirmation, a hiring manager's review) carry a token signed with HMAC-SHA256, scoped to its one purpose and given an expiry, from 7 days for a privacy-request confirmation to a year for an unsubscribe link. A link minted before expiries were added carries none and is still honoured.
- Incoming webhooks are checked against their sender's signature before anything is written: Stripe, Resend, Slack, Zoom and Twilio. SignalHire's answer is accepted only at a callback address carrying a signature minted for that one lookup.
Fetching a page someone points at
When the server fetches a public page (a pasted job posting, a company's site, a site a person linked), it first resolves the host and refuses private, loopback, link-local and reserved addresses and any port but 80 and 443, and it checks every redirect again before following it.
Limits
| What | Limit |
| AI requests | 40 per person an hour |
| Search operations | 40 per person an hour |
| Contact lookups | 10 per person an hour; 300 on an unlimited lookup plan, a ceiling a person cannot reach by hand |
| Sign-in links | 5 per address and 20 per network address an hour |
| Privacy requests | 5 an hour from one network address |
| Job applications | 6 an hour from one network address |
These are cost and abuse backstops, not access control: the AI, search and sign-in link limits let a request through when the database cannot be read, so a hiccup never stops a recruiter mid-search or locks anyone out of signing in. The contact lookup ceilings are defaults the operator can change in the deployment's settings. Exports are watched too: an account that makes 25 exports, pulls 5,000 records, or exports from 4 different network addresses in a day is flagged to the operator.
The audit trail
- Every decision that affects a candidate writes a receipt to an append-only chain, and sensitive actions write to an audit table chained by HMAC-SHA256. Both are described on the compliance page.
- Every export of a list writes a row saying who, what, how many records and from which network address.
What is logged
- Each session records the browser's user agent and network address, and the account keeps the network address of its last sign-in.
- Each sign-in link request records the address and the network address, for the rate limit.
- Privacy requests and job applications record a keyed hash of the network address, not the address itself.
- One row per AI call and per search, for the hourly limits and cost reporting.
- The server's own log lines (on Vercel) record errors and events; the main email sender masks the recipient's address in its lines. Server errors are also emailed to the operator.
- Most pages, the signed-in desk included, load Vercel Speed Insights, which reports the page address and load timings to Vercel. It is left off every page whose path carries a token. Pages that carry a token in the query string instead (the sign-in link, a hiring manager's feedback link, a candidate's link to pick a time) still load it.
Keeping customers apart
- Every read is scoped to the signed-in desk, or to the workspace a desk belongs to. One workspace's people are never shown to another.
- What the engine learns from a desk's likes and passes is keyed to that desk and never read by another.
- Anyone who opted out is checked for before a person is researched, added to a pipeline, looked up or sent anything.
Browser protections
- Every response sets
X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin and a Permissions-Policy that switches off the camera and location.
- A Content Security Policy is sent in report-only mode: it is written, and browsers report what it would block, but it is not enforced yet.
Not in place yet, plainly:
- No SOC 2 report, no ISO 27001 certificate, and no published penetration test.
- No multi-factor sign-in of SourcingNav's own, and no SAML single sign-on or SCIM provisioning. Signing in with Google or Microsoft carries whatever that account requires.
- The Content Security Policy is report-only, not enforced.
- A sign-in link works until it expires; it is not cancelled on first use.
- An emailed action link minted before expiries were added has no expiry and is still accepted.
- The Google Calendar refresh token is not encrypted at the application level.
- A database restore drill is on the roadmap and has not been run.
- Speed Insights is not kept off the pages that carry a token in the query string, the sign-in link among them.
Reporting a problem
Email hello@sourcingnav.com with what you found and how to reproduce it.