You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TL;DR Hiring is a two-way street: a poster wants to hide a job offer from specific viewers just as a member (#938) hides "I'm looking" from their employer. Add a per-posting exclusion list — members, organization pages (via #929), and email domains — subtracted from whoever would otherwise see the posting on the board, its detail page, agent formats, and job alerts. Verified organizations also get a standing default list applied to all their postings. Shares #938's exclusion seam.
Where: the job-posting resource (#932) gains the exclusion list + a small editor and a base everyone/members visibility (added to #932); the standing organization default rides on organization settings (#929/#930); enforced at the public board (#933), the /jobs/:slug detail page + its .md/.txt/.json/.xml siblings, saved-search alerts (#935), and the /api/2.0 jobs endpoints (#936). Reuses the per-subject viewer-exclusion predicate #938 introduces.
Now:#932/#933 make every posting maximally reachable — the point of a board — with only per-posting seo?/geo? crawl toggles. There is no way to keep a posting on the board for the world except a chosen few. Real hiring frequently needs exactly that:
Your own staff — backfilling a role before the incumbent is told, or hiring a layer above an existing team. Exclude @ourorganization.example and/or your own organization page (which also covers staff whose linked work experience points at it, see the organization dimension below) so employees don't stumble over the posting.
Competitors — a job ad leaks strategy (the team you're building, the market you're entering). Exclude a rival organization / its domain so their people can't read your plans off the board.
Specific people — a known bad-faith applicant, a harasser, or the very person being quietly replaced.
Base visibility (resolved — was the open question):#932 gains a posting visibility: everyone | members (default everyone, so current reach is unchanged). members shows the posting only to signed-in members and forces it out of the crawlable/JSON-LD/sitemap surfaces regardless of seo?. This is what makes exclusion airtight against a logged-out competitor: pair members with the exclusion list to hide a posting from a rival even when they aren't signed in. everyone keeps today's public behavior, and then exclusion can still only reduce the signed-in on-platform audience — helper text says so plainly.
Want: a per-posting exclusion list subtracted as the last step of the posting's visibility check, on the same three dimensions as #938 (subtract never adds):
Members: exclude a specific member account.
Organizations (via Jobs 2/9: Verified company pages at /companies (domain-proof only, no unverified pages) #929 verified pages): exclude an organization — covers its verified domains (with subdomains, per the domain rule), its role-holder accounts, and members whose current work experience is linked to it (Jobs 4/9: Link work experiences to verified company pages (optional, free text stays) #931). Members usually do confirm their organization e-mail, so the domain rule carries most of the weight; the work-experience link is belt-and-braces for the rest — staff who registered with a personal address only, or a contractor without an organization mailbox. Self-linking only ever reduces what the linker sees, so this dimension cannot be gamed.
Domains: exclude an email domain. A signed-in viewer is excluded when any of their confirmed emails is at that domain or any subdomain of it (host suffix match: example.com also matches @eu.example.com — no public-suffix list needed), regardless of the email's public? flag; unconfirmed emails never match. Logged-out/anonymous viewers have no email, so a domain rule only narrows the signed-in on-platform audience.
The poster, and (if organization-attributed) the owning organization, always see their own posting; neither can be excluded from it.
Organization default exclusions (in scope): a verified organization sets a standing exclusion list once in its organization settings (#929/#930) — e.g. competitor domains it never wants to advertise vacancies to. Every posting attributed to that organization inherits it: the effective exclusion set for an organization posting is the organization default ∪ the posting's own list (union; the per-posting editor can add but not un-inherit an organization default). Personal postings have no organization default. The default is editable by organization role-holders and takes effect immediately on all live postings.
Silent by design (mirrors Social.block_user): an excluded viewer gets the same not-found/hidden result as any non-permitted viewer — no signal they were singled out. Exclusion is visibility-only.
Full block implies exclusion: if the posting's responsible member has blocked a viewer via Social.block_user, that viewer is automatically excluded from the posting too.
Enforcement surfaces (each hides an excluded signed-in viewer):
The /jobs/:slug detail page and its .md/.txt/.json/.xml siblings → an excluded signed-in viewer gets the not-found/hidden path exactly like a non-visible posting; anonymous viewers fall through to the posting's base visibility.
Bookmarks/likes: a viewer excluded after saving a posting no longer sees it in their /bookmarks hub, like a removed posting.
Reusable seam: generalize #938's per-member viewer-exclusion into a per-subject one — subject = a member's job-search fields or a job posting — behind one excluded?(subject, viewer) predicate (viewer is a listed member, belongs to a listed organization — role or current linked work experience — has a confirmed email at a listed domain/subdomain, or is blocked by the subject's owner) and one three-dimension entry model. A posting resolves its effective set as own-rows ∪ organization-default rows; no bespoke matching logic per side.
Defaults: empty list = harmless default (posting fully visible per its base visibility), per the milestone's harmless-default rule.
Cleanup: exclusion rows (per-posting and organization-default) are removed when the posting, the owning member, an excluded member, or an excluded/owning organization is deleted; the Accounts.delete_user/1 deletion chokepoint is extended for member rows on either side.
Validation: domain shape (lowercase host, no scheme/path/@), dedupe, a per-posting and per-organization cap (e.g. 200 entries each), cannot exclude the poster or the owning organization.
Docs: the postings subsystem doc created by #932 — base visibility, exclusion + organization-default semantics, silent/visibility-only rule, block integration, and the honest on-platform-only limit for everyone postings.
Smoke test (browser, per CLAUDE.md): publish a posting (visibility everyone); from a second signed-in account whose confirmed e-mail is at eu.x.example, it appears on /jobs, in search, and on /jobs/:slug, and (with a matching saved search) would alert; add domain x.example to the posting's exclusion list → gone from that account's board, search, and detail (/jobs/:slug → not found), and no alert fires, while a third account on another domain still sees it; a logged-out /jobs/:slug.md still shows it (public) — then flip the posting to members and confirm it leaves the public/crawl surfaces entirely; set the same domain as a organization default and confirm a second organization posting inherits it; swap the domain rule for the second account added by @handle, and separately by blocking it → same result each time; exclude a verified organization and give a fourth account (no organization e-mail) a current work experience linked to it → posting hidden for that account too; check the German render (Accept-Language: de).
Depends on:#932 (the posting resource + the new base visibility it attaches to) and #933 (the board, the primary read surface); #929/#930 for the organization dimension and the organization-default settings UI (members + domains enforce without them). Best sequenced right after #933; shares the seam #938 introduces.
TL;DR Hiring is a two-way street: a poster wants to hide a job offer from specific viewers just as a member (#938) hides "I'm looking" from their employer. Add a per-posting exclusion list — members, organization pages (via #929), and email domains — subtracted from whoever would otherwise see the posting on the board, its detail page, agent formats, and job alerts. Verified organizations also get a standing default list applied to all their postings. Shares #938's exclusion seam.
Where: the job-posting resource (#932) gains the exclusion list + a small editor and a base
everyone/membersvisibility (added to #932); the standing organization default rides on organization settings (#929/#930); enforced at the public board (#933), the/jobs/:slugdetail page + its.md/.txt/.json/.xmlsiblings, saved-search alerts (#935), and the/api/2.0jobs endpoints (#936). Reuses the per-subject viewer-exclusion predicate #938 introduces.Now: #932/#933 make every posting maximally reachable — the point of a board — with only per-posting
seo?/geo?crawl toggles. There is no way to keep a posting on the board for the world except a chosen few. Real hiring frequently needs exactly that:@ourorganization.exampleand/or your own organization page (which also covers staff whose linked work experience points at it, see the organization dimension below) so employees don't stumble over the posting.Base visibility (resolved — was the open question): #932 gains a posting
visibility: everyone | members(default everyone, so current reach is unchanged).membersshows the posting only to signed-in members and forces it out of the crawlable/JSON-LD/sitemap surfaces regardless ofseo?. This is what makes exclusion airtight against a logged-out competitor: pairmemberswith the exclusion list to hide a posting from a rival even when they aren't signed in.everyonekeeps today's public behavior, and then exclusion can still only reduce the signed-in on-platform audience — helper text says so plainly.Want: a per-posting exclusion list subtracted as the last step of the posting's visibility check, on the same three dimensions as #938 (subtract never adds):
example.comalso matches@eu.example.com— no public-suffix list needed), regardless of the email'spublic?flag; unconfirmed emails never match. Logged-out/anonymous viewers have no email, so a domain rule only narrows the signed-in on-platform audience.Organization default exclusions (in scope): a verified organization sets a standing exclusion list once in its organization settings (#929/#930) — e.g. competitor domains it never wants to advertise vacancies to. Every posting attributed to that organization inherits it: the effective exclusion set for an organization posting is the organization default ∪ the posting's own list (union; the per-posting editor can add but not un-inherit an organization default). Personal postings have no organization default. The default is editable by organization role-holders and takes effect immediately on all live postings.
Silent by design (mirrors
Social.block_user): an excluded viewer gets the same not-found/hidden result as any non-permitted viewer — no signal they were singled out. Exclusion is visibility-only.Full block implies exclusion: if the posting's responsible member has blocked a viewer via
Social.block_user, that viewer is automatically excluded from the posting too.Enforcement surfaces (each hides an excluded signed-in viewer):
/jobs/:slugdetail page and its.md/.txt/.json/.xmlsiblings → an excluded signed-in viewer gets the not-found/hidden path exactly like a non-visible posting; anonymous viewers fall through to the posting's base visibility./api/2.0jobs endpoints → respect the exclusion for the authenticated member. The outboundjob.publishedwebhook fires to the poster's own integrations, so it is not viewer-scoped (n/a)./bookmarkshub, like a removed posting.Reusable seam: generalize #938's per-member viewer-exclusion into a per-subject one — subject = a member's job-search fields or a job posting — behind one
excluded?(subject, viewer)predicate (viewer is a listed member, belongs to a listed organization — role or current linked work experience — has a confirmed email at a listed domain/subdomain, or is blocked by the subject's owner) and one three-dimension entry model. A posting resolves its effective set as own-rows ∪ organization-default rows; no bespoke matching logic per side.Defaults: empty list = harmless default (posting fully visible per its base visibility), per the milestone's harmless-default rule.
Cleanup: exclusion rows (per-posting and organization-default) are removed when the posting, the owning member, an excluded member, or an excluded/owning organization is deleted; the
Accounts.delete_user/1deletion chokepoint is extended for member rows on either side.Validation: domain shape (lowercase host, no scheme/path/
@), dedupe, a per-posting and per-organization cap (e.g. 200 entries each), cannot exclude the poster or the owning organization.Docs: the postings subsystem doc created by #932 — base visibility, exclusion + organization-default semantics, silent/visibility-only rule, block integration, and the honest on-platform-only limit for
everyonepostings.Smoke test (browser, per CLAUDE.md): publish a posting (visibility
everyone); from a second signed-in account whose confirmed e-mail is ateu.x.example, it appears on/jobs, in search, and on/jobs/:slug, and (with a matching saved search) would alert; add domainx.exampleto the posting's exclusion list → gone from that account's board, search, and detail (/jobs/:slug→ not found), and no alert fires, while a third account on another domain still sees it; a logged-out/jobs/:slug.mdstill shows it (public) — then flip the posting tomembersand confirm it leaves the public/crawl surfaces entirely; set the same domain as a organization default and confirm a second organization posting inherits it; swap the domain rule for the second account added by @handle, and separately by blocking it → same result each time; exclude a verified organization and give a fourth account (no organization e-mail) a current work experience linked to it → posting hidden for that account too; check the German render (Accept-Language: de).Depends on: #932 (the posting resource + the new base visibility it attaches to) and #933 (the board, the primary read surface); #929/#930 for the organization dimension and the organization-default settings UI (members + domains enforce without them). Best sequenced right after #933; shares the seam #938 introduces.