Job Board
Job Board
Section titled “Job Board”Status: Phase 0 scaffolded —
apps/jobboardAxum service boots,jobboard.protowired into the build. Auth = Supabase (JWTsub=auth.users.id); the Phase-0 first-party auth/sessions in the Rust code is a dead end to be removed. Schema migration shipped (20260615120000_jobboard_tables.sql): 23 tables, ULID-as-uuid PKs, junction tables (no array refs), state/money/lifecycle CHECK constraints, RLS lockdown,updated_attriggers; all user refs →auth.users(id). No seed yet. Phases 1–10 below remain spec.
A curated, both-sides-vetted job board for freelance work — think Upwork/Fiverr, but gated and quality-first. Sanctioned members post gigs, other sanctioned members take them up, and the platform handles discovery, matching, and messaging. Money moves off-platform in v1 (lead-gen model); monetization is built behind abstraction hooks to switch on later.
Game development is the launch vertical, not the ceiling. We pioneer in game dev (art, code, audio, design, QA for games) because it’s an underserved, portfolio-driven niche where curation wins — but the data model, taxonomy, and matching are vertical-agnostic by design. Adding a new vertical (e.g. video production, music, dev/IT, design, writing) is a data operation (seed a Vertical + its taxonomy), not a schema change or code change.
Architecture is decoupled: a standalone Rust/Axum REST API (own DB access, own first-party auth — no third-party auth SaaS) serving a separate frontend client (framework TBD) over HTTP. This keeps the API reusable across web, future mobile, and integrations.
Positioning / moat
Section titled “Positioning / moat”- Curation is the wedge. Both posters and takers are vetted, so quality is the product. Every gig is legit; every applicant is screened.
- Vertical-first go-to-market, horizontal data model. We win one vertical at a time (game dev first) by speaking its language, while the underlying platform stays general so we can expand without re-platforming.
- Portfolio-first. Talent in creative/technical verticals is hired by seeing work (characters, gameplay clips, playable builds, reels, repos), not reading résumés. The portfolio system is a first-class feature across every vertical.
Personas & roles
Section titled “Personas & roles”| Persona | What they do | Vetting |
|---|---|---|
| Poster | Has work (indie dev, studio, publisher, agency, business). Posts gigs, reviews applicants, selects talent. | Vetted (org/identity + intent) |
| Taker | Does work (artist, programmer, audio, designer, QA, and every other vertical’s specialists). Builds a portfolio, applies to gigs. | Vetted (portfolio review) |
| Admin | Platform staff. Reviews membership applications, moderates gigs, manages verticals + taxonomy, handles reports. | Internal |
- A single account can hold both capabilities (
posterandtaker), granted independently during vetting. - Capabilities are the authorization primitive — encoded as a bitmask on the existing
User.rolefield (see protobuf section). - A member can operate in one or more verticals; their granted disciplines/skills are scoped per vertical.
Verticals & taxonomy
Section titled “Verticals & taxonomy”The platform is organized around verticals (top-level service areas) and a controlled, admin-managed vocabulary scoped to each vertical — so filtering and matching work without free text, and so a new vertical can be launched without touching code.
- Vertical — the top-level service area. Game Dev is vertical #1. Others (Video, Audio/Music, Software/IT, Design, Writing, Marketing…) are added later as data.
- Discipline — a category of work within a vertical (data-driven taxonomy, not a hardcoded enum).
- Tool/Platform — engines, software, frameworks relevant to the vertical.
- Skill tag — finer-grained, controlled-but-extensible.
Why no hardcoded discipline enum? An earlier draft baked game-dev disciplines into a protobuf bitmask. That ties the schema to one vertical. Instead, disciplines/tools/skills all live in the
Taxonomytable, each row scoped byvertical_id. Gigs and profiles reference taxonomy ids, so a new vertical is purely a seed-data operation. (If a vertical later needs ultra-fast bitmask filtering, a per-vertical cached bitmask can be derived from taxonomy ids without changing the contract.)
Game-dev vertical — seed vocabulary (example)
Section titled “Game-dev vertical — seed vocabulary (example)”This is the first vertical’s seed data, illustrating the pattern — not a fixed part of the schema:
- Disciplines: 2D Art, 3D Art, Animation/Rigging, Programming, Technical Art/Shaders, Audio (Music/SFX/VO), Game Design, Level Design, Narrative/Writing, UI/UX, QA/Testing, Porting, Localization, Production/PM, Community.
- Tools/Engines: Unity, Unreal, Godot, GameMaker, Bevy, Custom; tools — Blender, Maya, ZBrush, Substance, Photoshop, Spine, FMOD, Wwise.
- Skill tags:
netcode,humanoid-rigging,pixel-art,procgen,shader-graph,vertical-slice.
Admin-managed vocabulary so new verticals, disciplines, tools, and skills can be added without code changes.
Core flows
Section titled “Core flows”A. Membership / vetting (both sides)
Section titled “A. Membership / vetting (both sides)”- Sign up (first-party auth — email/password or OAuth).
- Choose intended capabilities: post work, take work, or both; and the vertical(s) of interest.
- Complete application — statement + portfolio links; talent pick vertical(s) + disciplines/tools, posters add org info.
- Submit → application status
pending. - Admin reviews in the vetting queue → approve/reject per capability.
- On approval → capability bit set on
User.role; user unlocks profile + the granted actions.
B. Poster posts a gig
Section titled “B. Poster posts a gig”- Approved poster creates a gig (title, vertical, discipline(s), scope, budget, tools, skills, deadline).
- Publish →
OPEN(optionallyPENDING_REVIEWif gig moderation is enabled). - Gig enters discovery.
C. Taker discovers & applies
Section titled “C. Taker discovers & applies”- Browse / search / filter gigs (vertical, discipline, tool, budget…).
- View gig detail.
- Apply — cover message, proposed rate, attach relevant portfolio items (gated on
taker). - Poster notified.
D. Selection & engagement
Section titled “D. Selection & engagement”- Poster reviews applicants, shortlists, messages candidates.
- Poster accepts one →
Engagementcreated, gig →FILLED, others auto-declined/notified. - Work + payment happen off-platform (v1).
- Either party marks the engagement complete.
- Both leave reviews → reputation accrues (aggregated into
User.reputation).
E. Admin / moderation (cross-cutting)
Section titled “E. Admin / moderation (cross-cutting)”- Vetting queue, gig takedowns, report handling, vertical + taxonomy management, audit log.
Data model (Postgres)
Section titled “Data model (Postgres)”Accessed via sqlx (async, compile-time-checked queries; migrations via sqlx migrate). Each table is mirrored by a protobuf message (see below). Reused entities map to existing protos; new entities to the jobboard package.
| Table | Source proto |
|---|---|
auth.users (identity) | Supabase — referenced by user_id FKs, not recreated |
profiles (base) | reuse Profile (kbveproto) |
verticals | new jobboard.Vertical |
talent_profiles | new jobboard.TalentProfile |
client_profiles | new jobboard.ClientProfile |
member_applications | new jobboard.MembershipApplication |
portfolio_items | new jobboard.PortfolioItem (+ reuse MediaAttachment) |
taxonomy (disciplines/tools/skills, scoped per vertical) | new jobboard.Taxonomy |
gigs | new jobboard.Gig |
applications | new jobboard.Application |
engagements | new jobboard.Engagement |
reviews | new jobboard.Review |
conversations, messages | new jobboard.Conversation / Message (+ reuse MediaAttachment) |
notifications | new jobboard.Notification |
reports, audit_log | new (admin) |
invoices, settings, globals (monetization hooks) | reuse Invoice, Setting, Global (kbveproto) |
Tech architecture (decoupled)
Section titled “Tech architecture (decoupled)”Backend — Rust API service
Section titled “Backend — Rust API service”| Concern | Choice | Why |
|---|---|---|
| Framework | Axum (Tokio) | Fast, ergonomic, tower middleware ecosystem |
| Auth | Supabase — verify the Supabase JWT (sub = auth.users.id, kbve_username claim), no first-party user/session store | Reuses existing KBVE identity; same model as apps/rows |
| DB access | jedi PgCluster (tokio-postgres + bb8, rw/ro/any split, RLS caller-scoped reads, valkey cache) | House pattern (matches axum-kbve); free read/write split + RLS + caching |
| Migrations | dbmate (packages/data/sql/dbmate/migrations) — schema deferred, not committed yet | Repo-standard migration tool; one system across the monorepo |
| DB | Postgres — KBVE CNPG/Supabase cluster via KBVE_PG_RW_URL | Shared cluster; jobboard schema isolates the tables |
| Wire format | prost/tonic protobuf over jedi.JediEnvelope | Reuses the KBVE proto/envelope transport |
| Media | S3-compatible (Cloudflare R2) | Rust-friendly; private deliverables |
| Search | Postgres FTS (v1) → Meilisearch later | Meilisearch is Rust-native, easy upgrade |
| Notifications | In-app (DB) v1; email later | Keep v1 lean |
Frontend — TBD (decided in a later phase)
Section titled “Frontend — TBD (decided in a later phase)”- Pure HTTP client of the API. Candidates: Astro + React (KBVE-native), Next.js (frontend-only), or React SPA.
- Auth carried as httpOnly session cookie (browser) — already supported by the API design.
Authorization model: Axum middleware/extractors authenticate the session/token; route guards check the Capability bitmask on User.role (POSTER/TAKER/ADMIN); ownership checks (e.g. gig owner) enforced per-handler.
Deployment
Section titled “Deployment”The deployable surface lives in apps/jobboard: Dockerfile (chiseled Ubuntu axum base), version.toml, and nx project.json (build / test / lint / run / container / containerx). The container build context is the repo root.
Container image
Section titled “Container image”- Built via
./kbve.sh -nx jobboard:container→kbve/jobboard:latest(+ version tag); production config also tagsghcr.io/kbve/jobboard. - Multi-stage cargo-chef build (planner → cook → builder → runtime) on
ghcr.io/kbve/chisel-ubuntu-axum. Runtime image runs/app/jobboardas uid 10001, exposes5400. - Protos are vendored (
apps/jobboard/src/proto/jobboard.rs); the image build does not runprotoc. Regenerate only when the.protochanges:BUILD_PROTO=1 cargo build -p jobboard(and revert the incidental jedi proto regen before committing).
Runtime environment (env var contract)
Section titled “Runtime environment (env var contract)”| Var | Required | Default | Notes |
|---|---|---|---|
KBVE_PG_RW_URL | yes | — | jedi PgCluster primary (rw). DATABASE_URL is mapped to it if unset (dev convenience). |
KBVE_PG_RO_URL | no | = rw | Read replica; falls back to rw. |
KBVE_PG_ANY_URL | no | = rw | Any-instance read fallback. |
HTTP_HOST | no | 0.0.0.0 | Bind host. |
HTTP_PORT | no | 5400 | Bind port. |
SECURE_COOKIES | no | false | Set true behind TLS (sets Secure on the session cookie). |
RUST_LOG | no | jobboard=info,tower_http=info | Tracing filter. |
jedi PgCluster also honors its standard KBVE_PG_* tuning knobs (pool sizes, timeouts, search_path, pooler rewrite, CA bundle) — see packages/rust/jedi/src/state/pg.rs. TLS bypasses transparently when the URL carries sslmode=disable (in-cluster), engages for sslmode=require (external).
Probes
Section titled “Probes”- Liveness:
GET /health(always 200 when the process is up). - Readiness:
GET /ready(200 when the DB cluster health probe passes, 503 otherwise) — gate traffic on this.
Kubernetes / ArgoCD (shipped)
Section titled “Kubernetes / ArgoCD (shipped)”Deployed under apps/kube/jobboard/ into the kbve namespace (own ArgoCD app):
jobboard-deployment.yaml— Deployment (ghcr.io/kbve/jobboard:<ver>, port 5400, liveness/health, readiness/ready, Reloader) +jobboard-service(ClusterIP 5400). Reuses the kbve-nssupabase-sharedsecret forKBVE_PG_RW_URL/_RO/_ANY(no new ExternalSecret).httproute.yaml—jobs.kbve.com→jobboard-service:5400, parented tokbve-gateway. No cert/ReferenceGrant/gateway-listener needed:jobs.kbve.comis covered by the existinghttps-kbvewildcard listener (*.kbve.com, certkbve-wt-tls).jobboard-serviceaccount.yaml—jobboard-sa,automountServiceAccountToken: false. No RBAC Role/RoleBinding — the pod never calls the k8s API.application.yamlregistered inapps/kube/kustomization.yaml(kube-root app-of-apps).- ArgoCD apps track
main; PRs land ondev, so the manifest reaches the cluster only after the periodic dev→main release.
MDX-driven image-tag automation
Section titled “MDX-driven image-tag automation”The frontmatter above (version, image, version_toml, version_target, deployment_yaml) registers jobboard in .github/ci-dispatch-manifest.json (regenerate via astro-kbve:build → astro-kbve:sync:ci-manifest; registry entry in @kbve/devops registry.ts). On a version: bump here, ci-docker.yml’s version gate (MDX version > version.toml) builds + pushes ghcr.io/kbve/jobboard:<ver>, then utils-post-publish.yml opens an atom PR that sed-updates version.toml, Cargo.toml, and the deployment image tag in jobboard-deployment.yaml. First publish uses the version.toml = 0.0.1 sentinel so the gate fires.
Database schema
Section titled “Database schema”Source of truth: packages/data/sql/schema/jobboard/jobboard.sql — the canonical desired-state DDL (same convention as schema/meme, schema/ows, etc.). The dbmate migration packages/data/sql/dbmate/migrations/20260615120000_jobboard_tables.sql is the applied delta that brings prod to that state, generated from / kept identical to the schema file. The proto (packages/data/proto/jobboard/jobboard.proto) is the third leg and tracks the same shape. 23 tables, hardened with constraints, junction tables, RLS lockdown, and updated_at triggers. No seed (separate follow-up migration).
- Identity is Supabase. No
jobboard.users/jobboard.sessions— every owner column isuser_id uuid references auth.users(id). The service trusts the Supabase JWT (sub); reputation/capabilities live on jobboard rows or are derived. - Primary keys: lookup tables (
verticals,taxonomy) usebigint generated always as identity; entity tables use ULID-as-uuid —uuid default public.gen_ulid()::uuid(the house generator’s 32-hex output casts to a 16-byte binary uuid whose time prefix makes it chronologically sortable → non-enumerable + cheap keyset feeds).user_idisuuid→auth.users. - Junction tables replace all
*_idsarrays (real FKs + uniqueness):talent_verticals,talent_taxonomy,client_verticals,member_application_verticals,portfolio_tags,gig_taxonomy,application_portfolio_items. Single-vertical entities (gig_taxonomy,portfolio_tags) carryvertical_idand use composite FKs togigs(id, vertical_id)/portfolio_items(id, vertical_id)+taxonomy(id, vertical_id), so a tag must belong to the entity’s own vertical (cross-vertical tagging is rejected).talent_taxonomyis also vertical-aligned — composite FKs totalent_verticals(user_id, vertical_id)+taxonomy(id, vertical_id), so a talent can only tag taxonomy within a vertical they’ve joined. - State integrity: CHECK constraints on every enum/bitmask column (ranges from the proto enums —
budget_type0–3; gig/notificationstatus/kind in (0,1,2,4,8,16)treated as single-value enums;reports.target_kind in (1..4)=ReportTarget,reports.status in (0..3)=ReportStatus), money invariants (min ≤ max,≥ 0,currency ~ '^[A-Z]{3}$', minor units), identifier/text rules (slug/name^[a-z0-9]+(-[a-z0-9]+)*$— no leading/trailing/double hyphens; non-empty boundedlabel/title/reason/action), lifecycle invariants (member-app review state, one-pending-per-user partial unique, gig publication state — OPEN/closed states requirepublished_at, engagementstatus↔completed_atlifecycle [COMPLETED⇒completed_at ≥ started_at, else null] + one-per-gig, null-safe distinct-parties / distinct-reviewer-reviewee so double account deletion can’t trip the check, participantleft_at ≥ joined_at), unique review-per-(engagement, reviewer), message content non-empty + size caps (body ≤ 20k, ≤ 10 attachments),jsonb_typeofchecks. - Messaging:
conversation_participantshas a surrogateidPK; messages reference the participant via composite FK(conversation_id, sender_participant_id) → conversation_participants(conversation_id, id)withON DELETE NO ACTION— a participant row can’t be hard-deleted while it has messages (participants leave by settingleft_at, never by row deletion), yet a full conversation delete still cascades cleanly (NO ACTION defers to statement end; RESTRICT would break that). On account deletion the participant’suser_idisSET NULL(anonymized) and messages are preserved (they key off the immutable participant id, not the auth uuid). Per-participant read cursor (last_read_message_id, conversation-scoped composite FK with column-listSET NULL), not a singlemessages.read_at. - Indexes are feed/keyset-oriented: partial gig feeds
where status = 2(vertical_id, published_at desc, id desc+ global), applicant/poster application queues, message cursor, partial unread-notifications, reports queue, reviewee history, portfolio ordering (user_id, sort_order, created_at desc, id desc), member-application history + moderation queue, active-participant partial (user_id, conversation_id where left_at is null), and reverse indexes on every junction’s non-leading column. - Bounds + self-documentation: length caps on all free text (
headline/org_name≤200,about≤2000, descriptions≤5–20k,cover_message/body≤10k,portfolio_links≤20) andcomment on columnon every enum/bitmask column (GigStatus,EngagementStatus,ReportTarget, …) so the integer↔meaning mapping lives in the catalog and can’t silently drift from the proto/Rust enums. - Delete policy (middle ground): personal data
CASCADE(profiles, portfolio, notifications, member applications, conversation participants); commercial/moderation historySET NULL+ nullable (gigs.poster_id, applications.applicant_id, engagements, reviews, reports, audit_log, and conversation_participants.user_id) so the marketplace + chat record survives account deletion. Snapshot/display-name capture deferred. - RLS / privileges locked to a service-role-only model (the Rust service reaches the schema via its privileged jedi pool, NOT via PostgREST/
authenticated):create schema jobboard authorization postgres; RLS enabled on all 23 tables;forceon the strictly server-mediated tables (engagements,reviews,member_applications,reports,audit_log);revoke all on all tables/sequences from anon, authenticated; role-specificalter default privileges for role postgres … revoke(the generic form only covers the running role); schemarevoke from publicwith noauthenticatedusage grant — soauthenticatedgetspermission denied for schema jobboard(verified). Theset_updated_at()fn isrevoke … from public+owner to postgres. No plpgsql/PostgREST RPC layer — the jobboard Axum service is the single writer and reaches the schema via its jedi pool connecting as a BYPASSRLS role (service_roleorpostgres), which is required becauseforce row level securitybinds even the table owner (superusers/BYPASSRLS bypass it). RLS +forcetherefore act purely as defense-in-depth against any future direct/PostgREST path; the real authorization + multi-step invariants live in the service. (If direct client reads are ever wanted, add PostgREST grants + SELECT policies, orpublic.*proxy wrappers, per feature phase.) - Verified up + down on Postgres 17 (stub
auth.users/gen_ulid): 23 tables, 60 indexes, schema ownerpostgres; negative battery confirms cross-vertical tag, non-joined talent taxonomy, bad slug, empty label, over-length text, >20 portfolio links, unpublished-OPEN gig, COMPLETED-without-completed_at, dup review, empty message, cross-conversation cursor, badtarget_kind/target_id, andauthenticatedschema access all reject — while a full conversation delete cascades and a direct participant delete (with messages) is blocked, preserving history.
Enforced in the jobboard Axum service, not DDL (nullable historical FKs + temporal/identity rules can’t be plain CHECKs). The service is the single writer; each multi-step invariant is a Rust transaction (conn.transaction() + SELECT … FOR UPDATE on the gig/application rows), with the DB constraints as the safety net underneath — no plpgsql RPCs: portfolio-item ownership on application attach (portfolio.user_id = application.applicant_id); engagement creation (lock gig + accepted application, verify caller, reject competing applications, flip gig status, audit — one tx); reviews must have reviewer/reviewee ∈ engagement parties, reviewer ≠ reviewee, engagement COMPLETED; non-null identities on insert (the nullable id columns exist only for ON DELETE SET NULL, never for fresh rows); message send must reject left_at is not null participants (and optionally cursor-before-join); member-application review transitions; reports moderation; capability/role grants.
Other deferred follow-ups: seed migration; per-phase RLS SELECT policies; conversation inbox denormalization (conversations.last_message_at/id, maintained in the send RPC); attachment element-type validation (array-of-objects, RPC/helper); proto entity id fields uint64 → bytes (uuid) + Message.sender → participant id. The down (drop schema … cascade) is bootstrap-only — mark irreversible once prod data exists.
REST API surface (Axum)
Section titled “REST API surface (Axum)”All mutations go through the API; the frontend is a pure client. Auth via session cookie or Authorization: Bearer. Versioned under /api/v1.
# AuthPOST /auth/register email/password signupPOST /auth/login -> session cookie / tokenPOST /auth/logoutGET /auth/me current user + capabilitiesGET /auth/oauth/:provider start Discord/GitHub/Steam flowGET /auth/oauth/:provider/cb callback
# Verticals & taxonomyGET /verticals list active verticalsGET /verticals/:id/taxonomy disciplines/tools/skills for a verticalPOST /admin/verticals [admin] create verticalPOST /admin/taxonomy [admin] add discipline/tool/skill
# Membership / vettingPOST /applications submit membership applicationGET /admin/applications [admin] vetting queuePOST /admin/applications/:id/decision [admin] approve/reject per capability
# Profiles & portfolioGET /talent browse/filter talent (vertical, discipline, tool…)GET /talent/:usernamePATCH /me/profileGET/POST/PATCH/DELETE /me/portfolio[/:id]
# GigsGET /gigs browse/filter (vertical, discipline, tool, budget…)POST /gigs [poster] createGET /gigs/:idPATCH /gigs/:id [poster, owner]POST /gigs/:id/close [poster, owner]
# Applications to gigsPOST /gigs/:id/applications [taker] applyGET /gigs/:id/applications [poster, owner] list applicantsPATCH /applications/:id shortlist/decline/withdrawPOST /applications/:id/accept [poster] -> creates engagement, fills gig
# MessagingGET /conversationsGET /conversations/:id/messagesPOST /conversations/:id/messages
# Engagements & reviewsPOST /engagements/:id/completePOST /engagements/:id/reviews
# ModerationPOST /reportsGET /admin/reports [admin]Protocol Buffers
Section titled “Protocol Buffers”The platform’s wire/storage contract is protobuf. The proto root is packages/data/proto/ (the shared protoc include root — cross-package imports already work there). We add one new file — packages/data/proto/jobboard/jobboard.proto (package jobboard).
Phase 0 reality check. The reuse table below was aspirational; the actual repo differs, and Phase 0 ships
jobboard.protoas standalone (it defines its ownMediaAttachmentrather than importing one) to sidestep cross-package include friction. Corrections to the table: identity protos live inpackages/data/proto/kbve/kbveproto.protowith nopackagedeclaration (so they are awkward toimportinto a packaged file — reuse happens at the DB/service layer, not via proto import); there is nostatus.proto(MediaAttachmentwas invented locally; the closest existing iskbve.forum.Attachment); the Discord listing template iskbve/discordsh.proto(DiscordServer+ServerTag), notdisoxide.proto; there is nostore.proto/StoreService(onlyjedi/redis.proto’sRedisService).JediEnvelope/MessageKind/PayloadFormat(jedi/jedi.proto) andRedisServiceare real and reusable. As later phases need true cross-file reuse, givekbveproto.protoa package and import from the shared root.
What we reuse (do not recreate)
Section titled “What we reuse (do not recreate)”| Need | Reused proto | Where it lives | Notes |
|---|---|---|---|
| Auth / credentials | Auth | kbveproto.proto | Already has email, hash, salt, reset token+expiry, verification token+expiry, status, last_login, failed_login_attempts, lockout_until, 2FA secret, recovery codes. This is our first-party auth model — use as-is. |
| User identity | User | kbveproto.proto | userid, username, role, reputation, exp, created_at. We encode capabilities into role (bitmask) and aggregate review scores into reputation. |
| Base social profile | Profile | kbveproto.proto | name, bio, github, instagram, discord, unsplash, userid. Vertical-specific fields go in a new TalentProfile that references the same userid — we extend, not duplicate. |
| Bearer API tokens | Apikey | kbveproto.proto | For non-browser clients (Authorization: Bearer). |
| Monetization hooks | Invoice, Setting, Global | kbveproto.proto | Wired only when monetization switches on. |
| Media (portfolio/attachments) | MediaAttachment | status.proto | url, media_type (bitmask), caption. Embedded by PortfolioItem and Message. |
| Listing pattern | DiscordServer, DiscordTag | disoxide.proto | Template (not copied): bitmask status/categories, inline SQL CHECK comments, URL regex, updated_at. Gig, Vertical, and Taxonomy follow this convention. |
| Transport envelope | JediEnvelope, MessageKind, PayloadFormat | jedi.proto | Job board messages travel as payload bytes inside JediEnvelope. Existing MessageKind bits (AUTH, HTTP_API, WEBSOCKET, REDIS, SUPABASE) already cover routing — no new kinds needed for v1. |
| KV / cache / realtime | StoreService, RedisService | store.proto, redis.proto | Caching, presence, live message/notification fan-out. Reuse as-is. |
What we add — jobboard.proto
Section titled “What we add — jobboard.proto”Canonical source: packages/data/proto/jobboard/jobboard.proto (package jobboard, standalone, built via prost). It is not inlined here — that drifts; the proto is kept in sync with the SQL schema package (packages/data/sql/schema/jobboard/).
- Id conventions: entity ids are
bytes= the 16-byte ULID-as-uuid (public.gen_ulid()::uuid); user refs arebytes=auth.users(id); lookup ids (Vertical,Taxonomy) and the taxonomy/vertical id lists areuint64=bigintidentity; timestamps are ISOstring. - Messages mirror the tables: added
ConversationParticipant,Report,AuditLog;Messagecarriessender_participant_id(not a raw user id) and droppedread_at(read state is the participant cursor);Conversationdroppedparticipant_ids;ByIdkeys onbytes,ByLookuponuint64. - Taxonomy/vertical id lists (
discipline_ids/tool_ids/skill_ids/tag_ids/vertical_ids) stayrepeated uint64— the API view of the junction tables; relational integrity (same-vertical, ownership) lives in SQL.
Reuse summary
Section titled “Reuse summary”- New file:
jobboard.proto(1 file, 1 package). - Reused as-is:
JediEnvelope/MessageKind/PayloadFormat(jedi/jedi.proto),RedisService(jedi/redis.proto). (Identity reuse —Auth/User/Profileetc. — happens at the DB/service layer, not via proto import, sincekbveproto.protohas no package; see the reality-check note above.) - Reused as pattern (not copied):
kbve.discordsh.DiscordServer→Gig,ServerTag→Vertical/Taxonomy. - No new
MessageKindvalues — existing routing bits suffice for v1. - No hardcoded discipline enum — disciplines/tools/skills are data-driven taxonomy scoped by vertical, so new verticals require no proto change.
Trust & safety
Section titled “Trust & safety”- Vetting on both sides is the primary trust mechanism.
- Reporting + admin takedown for gigs, profiles, messages.
- Off-platform payment disclaimer + safety guidance (no escrow yet — set expectations clearly).
- Audit log for admin actions.
Key risks / open questions
Section titled “Key risks / open questions”- Disintermediation / leakage — with payments off-platform, members can match once and transact off-platform forever. Mitigations: keep reviews/reputation valuable enough to bring people back; consider gating contact details until a stage; plan the monetization switch before leakage becomes habit.
- Vetting throughput — manual approval is the quality bar and a growth bottleneck. Needs a written rubric (per vertical) + a fast admin UI.
- Cold-start liquidity — curated + both-vetted = slowest to fill. Likely need to hand-seed one side first, in one vertical first. Game dev launches first; which side to seed?
- “Sanctioned” criteria undefined — what objectively qualifies a poster vs a taker, per vertical?
- Vertical expansion sequencing — the model supports many verticals, but each needs taxonomy seeding, a vetting rubric, and liquidity. Which vertical follows game dev, and on what trigger?
- Proto include paths — sharing
MediaAttachment/identity across packages needs a common protoc include root; decide whether to relocate shared protos or vendor.
Phased build plan
Section titled “Phased build plan”| Phase | Scope | Outcome |
|---|---|---|
| 0 — Foundations | Axum + sqlx + Postgres; migrations; verticals + taxonomy seed (game dev first); first-party auth (register/login/session/logout) + CORS; wire jobboard.proto into the build | API boots, auth works, DB ready |
| 1 — Identity & vetting | Membership application endpoints, admin vetting queue, capability grants (per vertical) | Members can be sanctioned |
| 2 — Profiles & portfolio | Talent/client profiles, portfolio CRUD + R2 media upload | Public profiles exist |
| 3 — Gigs | Post gig, gig moderation, browse + filter (vertical/discipline/tool), gig detail | Work is discoverable |
| 4 — Applications & selection | Apply, applicant management, accept → engagement | Matching works end-to-end |
| 5 — Messaging | Conversations + threads + attachments | Parties can talk |
| 6 — Engagements & reviews | Mark complete, two-sided reviews, reputation display | Trust accrues |
| 7 — Frontend client | Choose framework; build the UI against the API | Usable product |
| 8 — Discovery & notifications | Search polish, saved items, in-app notifications | Retention loops |
| 9 — Monetization hooks | Wire entitlements to a chosen model (sub/featured/credits) via reused Invoice/Setting | Revenue switch ready |
| 10 — Second vertical | Seed a new vertical (taxonomy + rubric); validate the model expands without schema change | Multi-vertical proven |
Decisions
Section titled “Decisions”Locked: curated, both-sides vetted · gig→apply mechanic · lead-gen v1 (off-platform payments) · monetization-agnostic hooks · game dev as the launch vertical, vertical-agnostic data model (verticals + data-driven taxonomy, no hardcoded discipline enum) · decoupled architecture · Rust/Axum API · Supabase auth (verify JWT, sub = auth.users.id; all jobboard rows FK auth.users) — first-party auth was a wrong turn, removing it · jedi PgCluster (tokio-postgres + bb8) + dbmate migrations against the KBVE cluster · standalone jobboard.proto (one package, standalone for Phase 0).
Still open:
- Frontend framework (Astro+React / Next.js frontend-only / React SPA) — deferred to Phase 7.
- Sanctioning rubric for posters and takers (per vertical).
- Which side to seed first for liquidity (in the game-dev vertical).
- Which vertical follows game dev, and on what trigger.
- When/whether to gate contact info to reduce leakage.
- Eventual monetization model (subscription / featured / credits).
- cookie-only vs cookie+bearer; OAuth providers (Discord/GitHub/Steam) for v1.
- Proto include-path strategy (shared root vs vendor).