This project is a NestJS server backed by PostgreSQL 18 with pgvector. It serves JobStash search, account, subscription, historical funding, and operational APIs.
Welcome to the team! Let's get you situated.
Here are the steps you need to take to get the project setup on your local development environment.
- Download and run the
nodejsruntime for your respective os from here - Setup
nvmor any other Node Version Manager of your choice. Click me to see the nvm installation docs - Install Node 22 LTS with the node version manager from the previous step. The supported range is recorded in
package.json. - Clone the repo by clicking that nice green button that looks like this
- Yarn is the preferred package manager for the project, you can install it with
npm i -g yarnif you haven't already. To verify a correct installation runyarn --version - Run
yarn installin the project root directory to install all dependencies - Run
cp .env.example .envto setup the local env file for the project, don't worry the.gitignorefile is already configured to ignore it. Fill it up with the appropriate info. - Start the PostgreSQL service from the sibling
etlrepository withdocker compose --env-file /dev/null up -d postgres. The local database is exposed on127.0.0.1:5434. - Run
yarn start:devto start the dev server and head over tohttp://localhost:8080to access the app. - The documentation of the api is automatically available at
http://localhost:8080/apiwhen the server is started
P.S you can change the port the server listens on by setting the PORT env variable. By default it's set to 8080
- NestJS for the main server (REST style)
- PostgreSQL 18 and pgvector
- TypeORM for entity persistence and parameterized SQL for graph/search workloads
- PassportJS for auth/oauth
- Swagger for documentation
- Prettier for code formatting
GET /jobs/list and GET /jobs/feed accept organizationClassifications as a
comma-separated string or repeated query parameter. Values are combined with OR;
this filter is combined with all other job filters using AND. For example:
/jobs/list?organizationClassifications=ai,crypto&classifications=engineering.
The existing classifications parameter remains the job-role filter.
GET /jobs/filters publishes every canonical choice under
organizationClassifications.options, including choices with no current jobs:
ai (AI), crypto (Crypto), fintech (Fintech), robotics (Robotics),
banking (Banking), tech (Web2), out_of_scope (Out of scope), and
unclassified (Unclassified). Web2 is a display label, not a new stored value.
Unknown values receive the standard query validation error.
Filtering reads the existing vertical property of the actual projected
Organization or Project employer through its corresponding search document.
It does not inherit another entity's classification from a shared profile,
infer classifications from job titles, or treat a missing classification as
unclassified. Selection happens before results, group counts, and pagination;
multiple selections never duplicate jobs. No schema migration or classification
backfill is required.
Manually approved partner agencies receive job alerts, available placement
bounties, the GitHub candidate checker, and the opted-in talent pool at no charge.
Approval uses the existing access_workspaces.alerts_enabled flag; it never sets
entitlement_enabled, creates a Stripe subscription, changes the $299 paid Agency
contract, or grants a Recruiters.rip candidate subscription.
Apply scripts/sql/agency-alerts.sql to the existing graph/search/workspace
database before deploying this middleware revision. The migration is
idempotent and takes a write-blocking lock on graph_nodes and
job_search_documents while baselining all existing StructuredJobpost
identities, including offline/unprojected jobs, and installing the publication
trigger. Schedule this brief lock appropriately; do not remove or clear the
publication ledger on projection rebuilds or deployments. The trigger runs with
the projection writer's permissions; the migration grants the existing
jobstash_app role publication access. Any different ETL writer role also needs
INSERT on agency_alert_publications.
Existing configuration is reused: EMAIL, SENDGRID_API_KEY, FE_DOMAIN,
MW_DOMAIN, ORG_ADMIN_DOMAIN, and JWT_SECRET. Set ORG_ADMIN_DOMAIN to
https://admin.jobstash.xyz; MW_DOMAIN must be the externally reachable
middleware origin for email unsubscribe links. No new billing/environment
toggle is required. Delivery runs each minute only when
ENVIRONMENT=production and MIDDLEWARE_SCHEDULE_OWNER=1. Keep the existing
single-owner process convention; committed database claims also prevent
duplicate role sends across simultaneous workers.
Only a current SUPER_ADMIN can use GET/POST /agency-alerts/admin/workspaces.
Provisioning takes {primaryProfileId, ownerUserId, domain} and validates an
existing nonbanned EntityProfile and an existing account wallet. It creates or
reuses the matching workspace, records the approving administrator/time, and
does not infer agency status from a classifier. A profile/domain owned by a
different workspace cannot be reassigned through this endpoint.
POST /agency-alerts/admin/workspaces/:workspaceId/approval with
{enabled:false} revokes approval; {enabled:true} approves a workspace with an
available, nonbanned profile. Stripe billing status does not gate free Alert.
The admin member/remove-member POST endpoints accept existing account wallets
only, cannot replace/remove the owner, and are independent of paid access.
requireAgencyAccess authorizes the existing bounty-opportunities, available/top
talent, opted-in candidate-report, and GitHub candidate-report APIs for current
workspace members with either manual partner approval or active paid Agency
entitlement. Revoking approval removes free access immediately; an independent
active paid entitlement still grants the paid products. Removing membership denies
both paths. JWT permissions are refreshed from the account, not trusted as a way to
claim employer or administrator access.
The separate private-profile inspection/contact-reveal endpoints remain paid-only, with their existing member-role restrictions. Agency product access does not grant employer organization management, employer shortlists, or platform administration.
Members read GET /access-workspaces/:workspaceId/alerts, search
GET .../alerts/organizations?q=..., and use POST .../alerts/follows or
.../alerts/unfollow with {organizationId}. POST .../alerts/unsubscribe
removes all of that member's follows in that workspace. Read/stop controls remain
available after approval is revoked; search/follow require current approval.
Follows are personal, not a workspace-wide mailing list. No endpoint accepts a
recipient email or another member's identity. The recipient is the current
account's verified UserEmail; Privy email linking followed by check-wallet
refresh uses the existing verification sync. Missing email is reported as
emailAvailable:false. Changing/removing the selected email pauses existing
follows instead of silently redirecting them; explicitly unfollow/re-follow to
subscribe with the current verified email.
Email links offer a confirmation GET and signed POST unsubscribe; GET never changes preferences. One-click unsubscribe headers use that same POST endpoint. Unsubscribing never changes candidate email preferences or other members' follows. Membership deletion cascades personal follows, and delivery rechecks membership, nonbanned agency profile, whitelist approval, exact verified recipient, organization ownership, and current public-role visibility immediately before sending. Already handed-off messages cannot be recalled.
The trigger records each stable shortUUID only once, when its projection first
has public organization-owned visibility: online, not blocked, list-eligible,
tagged, public access, no direct Project owner, and no expert-organization
suppression. Direct Project jobs are not assigned to an inferred organization.
The effective first-public time is the later of discovery and the initial
publication timestamp; future-dated candidates become due without a catalogue
poll. A follow made after discovery but before that future publication is
eligible. Ordinary edits, reopening, retries, and projection reconstruction do
not generate another event. Follow/re-follow never backfills earlier public
roles. Roles withdrawn, reassigned, or no longer public at their delivery
recheck are skipped; an already consumed identity is not resent if reopened.
Each minute processes at most 20 organization/recipient batches of at most 50 roles, queried from indexed publication events. Normally delivery is attempted on the next minute tick after JobStash discovers the public role, not immediately when an external employer publishes it. Backlog, ETL delay, and provider delay can extend that time. First-public timestamps are fixed and are not rewritten by later metadata edits.
A delivery claim is committed before calling the existing SendGrid MailService.
It is never automatically reopened after a crash, timeout, or provider error:
an ambiguous provider response may already represent acceptance. This prevents
unconditional duplicate resends but can lose an alert on a pre-send crash or
failed attempt. agency_alert_deliveries.status distinguishes claimed,
accepted, skipped, and failed_unknown; accepted means provider acceptance,
not inbox receipt. Investigate failed/stale claims without blindly replaying
them. Existing SendGrid unsubscribe/suppression handling is not bypassed.
Run the Alert service/controller tests and the real-SQL
agency-alerts.integration.spec.ts with AGENCY_ALERTS_TEST_DATABASE_URL pointing
to a disposable localhost database whose name includes test. The fixture
isolates its schema and covers migration baseline/reapply, future publication,
deduplication across two workers, late commits, recipient changes, whitelist
revocation, removed members, and personal unsubscribe. It also exercises the real
HTTP authorization and PostgreSQL membership checks for approved/unpaid and paid
agency products, revocation with the same JWT, outsiders, forged permissions, and
private-profile/employer/admin boundaries. Bounty reads use actual SQL; candidate
payload retrieval is isolated at the service boundary. No integration fixture sends
email.
For an authorized, single-recipient staging smoke: sign in as a superadmin, approve an existing test agency profile and test account; sign in as that account, verify its email, follow a test organization, and publish two genuinely new public test roles through the normal ingestion path. Observe one claimed organization batch and provider acceptance, then independently check the controlled inbox. Edit/reopen those roles and confirm no additional attempt. Unfollow or revoke approval before another new role to confirm no send. Do not backfill existing customer follows or email real customers for verification.
Production changes are deployed from reviewed pushes to main. The existing
Coolify integration observes main and automatically builds the selected
revision; this repository does not create releases, tags, or deployment
identities.
- Make sure to update the
.env.examplefile anytime you add new env vars to the project - Please address linter warnings before pushing the code to the remote repo, all linter warnings will be treated as errors and will cause the checks to fail with errors.
- Remember to update the readme if any changes are made.
yarn build
export DATABASE_TEST_URL='<postgresql-test-url>'
yarn test:postgres
export DATABASE_URL='<populated-postgresql-url>'
yarn db:validate:postgres
yarn db:benchmark:postgresThe benchmark enforces a 750 ms p95 and execution-time ceiling against the populated local search projections. The complete migration and cutover runbook is in ../etl/docs/postgresql-cutover.md.
To discuss problems, shoot a message to the #discuss-problems channel on Slack
THREAT_INTEL gates org-admin's read-only observatory. Only a SUPER_ADMIN
may list, grant, or revoke it through GET, PUT, and DELETE
/users/threat-intel-access. These routes manage access only; threat source,
signals, and analyst state are not stored in middleware.
The additive companion surface uses the existing account JWT, PostgreSQL job projections and production Stripe merchant. Existing organization/agency routes and billing products remain unchanged. No private CV, evidence, scores, drafts, provider credentials or inference payloads are stored by this module.
Release order:
- Apply
scripts/sql/companion.sqlusing the normal schema-release credentials, after the graph/search schema. The transaction is repeatable and creates onlycandidate_devices,candidate_device_nonces,candidate_subscriptionsandcandidate_payment_events, with account foreign keys and application grants. No fixture memberships, account rewrites or data repairs are needed. - Reuse
STRIPE_API_KEYandSTRIPE_WEBHOOK_SECRET. SetSTRIPE_CANDIDATE_PRICE_IDto an active live USD 999-cent monthly recurring price, interval count one, without a trial. Keep existing Stripe products intact.RECRUITERS_PUBLIC_ORIGINmust behttps://recruiters.rip, independent ofORG_ADMIN_DOMAIN. The candidate website belongs torecruiters-rip-frontend; the downloadable app belongs torecruiters-rip-companion. Neither belongs to org-admin. No new merchant secret or frontend billing secret is introduced. - Keep the verified webhook at
POST /stripe/webhook. Enable checkout session completed/expired/asynchronous success/failure, invoice payment success/failure, and customer subscription created/updated/deleted events on that same endpoint. Candidate events are reconciled from Stripe live state; foreign charges are never assigned from webhook metadata. Duplicate events are transactionally ignored. Missing webhooks recover on status and signed data reads. - Deploy middleware and the additive recruiters frontend. The return URL is
/onboarding/membership/return; checkout is restricted tohttps://checkout.stripe.com. The account holder completes real checkout; deployment must not create a payment or grant access on their behalf.
Authenticated GET /profile/subscription returns {advanced,price,subscription};
POST /profile/subscription/checkout accepts {requestId} and returns
{checkoutUrl}. UUID request IDs also make checkout creation idempotent at Stripe.
POST /profile/subscription/cancel accepts only {requestId} and schedules
cancel_at_period_end:true on the authenticated candidate's verified Stripe
agreement. It returns {cancelAtPeriodEnd:true,paidUntil,cancelledAt} only after
Stripe confirms scheduling without changing the paid period. Current paid access
continues until that period ends; devices and private local records are not revoked.
Cancellation requests bind to their original checkout in the existing
candidate_payment_events ledger (cancellation.requested), so retries cannot
cancel a replacement agreement. No new schema, secret or environment value is required.
Billing lifecycle is separate from paid access: recoverable past_due, unpaid,
paused and incomplete Stripe agreements block a second checkout. Account holders
can recover payment through their existing Stripe invoice link or explicitly
cancel renewal from account settings; middleware never cancels it automatically.
Only expired checkout sessions and canceled/incomplete-expired subscriptions are
terminal. The idempotent schema also adds billing_state to existing metadata;
old rows start unresolved and must be provider-verified before replacement.
Terminal history is excluded from refresh. Concurrent owner reads share only
in-flight refresh work; completed results are not cached. Relevant agreement
reads retain row-lock ordering against webhook writes.
Pairing uses GET /profile/devices, POST /profile/devices/pair with
{requestId,deviceId,publicKey,label}, and POST /profile/devices/:id/revoke
with {requestId}. Keys are Ed25519 SPKI DER encoded as 59-character base64url.
The pairing reply is {deviceId,challenge,expiresAt,account:{id,label}}; the
native app must explicitly approve that account and sign activation.
The /privy/check-wallet response includes privyDid from the guard-verified
Privy user, so the frontend can seal the backend session to the displayed identity.
POST /device/activate, /device/heartbeat and /device/platform accept only
the existing Ed25519 raw-body protocol (x-device-id, x-device-time,
x-device-nonce, x-device-signature), with no cookie/bearer/query transport.
Requests expire after 30 seconds; nonces cannot be replayed. Request bodies are
limited to 16 KiB and platform responses to 8 MiB. An oversized original is
rejected, never truncated. Platform operations are status,
matching_catalogue, matching_jobs (at most ten canonical IDs) and job_read.
The catalogue uses publication time, newest first, for the latest fourteen days;
explicit canonical reads retain previously selected full originals and role
location evidence even when they age out of the catalogue.
Raw data requires an owned active device and a live paid subscription before and
after reads. A historical payment, test-mode object, trial, pending invoice,
expired membership or revoked device cannot authorize it. Provider failures fail
closed; they never extend a cached entitlement. Status remains available without
paid access. Parent release gates should include build, route manifest tests,
src/companion boundary tests, schema application review and the user-completed
live checkout/return/pairing flow.