← back to workProblemPeople are less likely to share exact salary and interview data when the platform can directly link each submission to their account.
ContributionI designed the privacy model and built the Go backend, Next.js PWA, CI pipeline, and deployment on Google Cloud Run.
ResultPre-launch product with the core privacy and contribution flows implemented.
SystemA Go modular monolith with Ent and PostgreSQL, Redis-backed Asynq workers, Atlas migrations, and deployment on Google Cloud Run.
Main challengeKeeping submissions anonymous while still enforcing access rules and preventing abuse.
Overview
The platform lets people share exact salary and interview data for the Turkish market in Turkish or English. Access expands from limited, stable previews to full reads after verification and one contribution.
Privacy boundary
The database does not store a direct key from a user account to an anonymous salary or interview entry.
The design prevents the application database from directly linking an account to an anonymous entry. It does not claim cryptographic anonymity against infrastructure logs, network metadata, timing correlation, or an operator with access to those systems.
Preview and timestamp handling
Salary previews use a coarse period selected by the contributor rather than an exact submission timestamp, reducing unnecessary correlation while keeping entries understandable.
Contribution-based access
The token is validated and consumed in the same transaction as entry creation, so malformed submissions do not spend it. The signed receipt can later grant contribution credit without storing a user-to-entry relationship.
Verification & badges
Work or student email verification uses a peppered six-digit OTP with a 10-minute TTL and five attempts. Only a unique peppered email HMAC is persisted. The current badge level is embedded in the submission token, and verification claims expire after twelve months.
Reads, tiers & anti-scraping
Anonymous visitors see three rows, registered users see ten, and full access requires verification plus one contribution. Values remain exact at every tier.
PostgreSQL selects a stable per-viewer preview using md5(seed||id), so refreshes do not rotate new rows into view and no preview table is required. Sensitive routes also use per-IP limits with explicit trusted-proxy handling; listings omit submission dates and use the coarse user-selected period for freshness.
Engineering & operations
The Go modular monolith separates bounded contexts through domain, application, infrastructure, and HTTP layers. Ent defines the schema; Atlas produces versioned migrations and CI rejects drift.
Asynq handles OTP mail, expiry sweeps, backfills, and password-reset work outside the request path. Static analysis, race tests, secret and vulnerability scans, plus privacy-specific tests gate three distroless Cloud Run images: API, worker, and migration job.
Key decisions & why
01No direct account-to-entry key
The application database does not store a direct relationship between an account and an anonymous entry. Infrastructure logs, network metadata, timing correlation, and operator access remain outside this database-level guarantee.
02User-selected salary periods
A coarse period such as 2026 H1 keeps salary data useful over time without recording the exact moment an entry was created. It represents the validity of the salary, not the submission event.
03Single-use tokens separate submission and credit
A single-use token lets an unauthenticated endpoint accept content; an HMAC-signed receipt returns contribution credit later. Validation and spending are transactional, so malformed requests do not consume tokens.
04Stable previews
Ordering by md5(seed||id) gives each viewer a stable subset without stored preview state. Refreshing does not reveal a new sample, reducing a simple enumeration path while keeping exact values visible.
05Modular Go monolith
One deployable keeps token spending and entry creation in a single transaction. Bounded contexts, Ent/Atlas drift checks, and privacy-specific tests protect those invariants as the code changes.