Needs to share one file quickly without creating an account or handing over personal data.
Outcome: upload-first flow with one-time, expiration, and preview rules set before submission.
About this project
anonshare is a non-commercial personal R&D project that behaves like a real product: real uploads, enforced access rules, background jobs, moderation controls, and an operator dashboard. It exists to validate a Bun-first stack in a realistic product context and to make the architecture and trade-offs legible to anyone evaluating the technical thinking.
Most file-sharing tools require an account, track usage, or bury access rules behind paywalls. Sometimes the real need is smaller and more immediate: send one file, explain how it should behave, and move on.
anonshare lets anyone upload a file, configure access rules such as one-time download, expiration, and preview, then receive a shareable link. The recipient opens the link and sees exactly what is available without needing an account on either side of the exchange.
This project is intentionally non-commercial. It exists to validate a Bun-first stack in a realistic product with storage, jobs, abuse controls, and admin operations while also being understandable in a portfolio review.
The page is written for four distinct readers: the person sharing a file, the person opening the link, the solo operator running the system, and the reviewer trying to assess the product and architecture quickly.
Needs to share one file quickly without creating an account or handing over personal data.
Outcome: upload-first flow with one-time, expiration, and preview rules set before submission.
Needs a clear answer when opening a link: preview, download, expired, hidden, deleted, or already consumed.
Outcome: public file pages make state explicit instead of collapsing every failure into a generic error.
Needs moderation, lifecycle visibility, queue health, and storage awareness in one place without building a support team around the product.
Outcome: a GitHub-protected dashboard with triage, queue telemetry, and anomaly review.
Needs to understand what the product does and why the architecture looks the way it does, without reading the whole repo first.
Outcome: this page turns implementation choices into an intentional, interview-ready narrative.
v1 is intentionally narrow. The target is not feature breadth; it is a fast anonymous flow, clear recipient state, and an operating model that one person can realistically understand and maintain.
Keep the path from landing page to copyable link short, while still making access rules explicit before upload begins.
Make recipients understand what happened to a file: available, expired, hidden, deleted, or consumed. State clarity matters as much as the download itself.
Favor boundaries, observability, and repairability so one person can run the system without silent failure becoming the default.
From upload to cleanup, the system moves a file through a defined lifecycle with explicit transitions rather than informal state hidden inside request handlers.
Validate file, MIME type, size, and options.
Persist metadata in PostgreSQL and stream the object into S3-compatible storage.
Generate an unguessable link with the configured access rules.
Download or preview, with one-time access enforced through a backend-controlled path.
Expiration jobs, cleanup, and reconciliation maintain correctness over time.
Reports, auto-hiding, and admin review reduce abuse exposure.
The monorepo splits responsibilities across three independently runnable processes and three shared packages. That boundary is deliberate: apps do not import each other, and shared rules live in packages instead of drifting across runtime surfaces.
apps/webTanStack Start with SSR. Serves the upload form, share pages, about page, and admin dashboard. The web app can consume shared contracts and domain helpers for display and validation, while mutations still go through the API boundary.
apps/apiHono on Bun. Handles upload, download, report, admin, and internal endpoints. Validates requests, orchestrates storage, and enforces access rules.
apps/workerBullMQ consumers on Bun. Processes expiration, cleanup, one-time post-download removal, and periodic reconciliation jobs. The reconciler is decomposed into independent passes (orphan detection, expired cleanup, pending promotion, one-time cleanup, terminal jobs, stale expiration, and cursor advancement) so each concern can be tested, retried, and observed in isolation.
packages/domainPure TypeScript state transitions, file status rules, and validation invariants. No infrastructure dependencies.
packages/contractsZod schemas for API requests and responses, error codes, and job payloads. Consumed by web, API, and worker without duplication.
packages/infrastructureDatabase connection, Drizzle schema and migrations, Redis client, S3-compatible storage adapter, rate limiter, logger, and config validation.
Each tool was selected for a specific operating reason, not just because it is currently fashionable. The stack is part of the product story.
Single runtime, package manager, bundler, and test runner. It reduces toolchain fragmentation and keeps the feedback loop fast enough for a solo project.
SSR, file-based routing, and type-safe loaders let the same web surface own public routes, share pages, and the admin shell without a second frontend architecture.
Lightweight HTTP boundary with clean middleware semantics and strong Bun support. The API stays explicit instead of disappearing into framework magic.
Relational storage with strong consistency guarantees for file state, reports, sessions, and anomalies. Drizzle keeps schema and migrations close to the code.
Redis backs rate limiting, queue state, and fast operational lookups. BullMQ handles delayed expiration, retries, cleanup, and recurring reconciliation.
Provider-agnostic object storage through Bun's native S3 API. MinIO works locally and production can target AWS S3 or Cloudflare R2 without rewriting the domain layer.
A single allowlisted GitHub identity protects the admin surface. That matches the real operating model better than inventing a multi-user auth system early.
The system is not presented as a toy upload demo. It includes the minimum operational posture needed to make anonymous sharing believable: observability, health checks, abuse limits, and a dashboard that surfaces lifecycle issues instead of hiding them.
API, worker, and operational scripts emit event-oriented logs with requestId, actor, entity, and outcome whenever those fields exist.
GET /health and bun run infra:check verify PostgreSQL, Redis, and storage contracts instead of assuming container health means application readiness.
Upload, report, and repeated link access are Redis-backed and degrade safely when Redis is temporarily unavailable, preserving UX where feasible.
The admin surface exposes queue lag, failed jobs, report pressure, and lifecycle anomalies so moderation and operational issues do not stay invisible. System settings such as the rate-limit degraded flag are surfaced separately so an operator can tell at a glance when a backing service is running in a reduced-capability mode.
Playwright tests run against the full local stack (web + API + worker + all dependencies) in CI. They cover the anonymous upload flow, preview and download gating on share pages, report-triggered auto-hide, and authenticated admin moderation and pagination flows — the cross-process behaviors that unit tests cannot reach.
Blind presigned URLs cannot guarantee atomic consumption. Retries, partial transfers, and concurrent requests create race conditions, so the backend reserves consumption before streaming the file.
Trade-off: Higher backend involvement than presigned-only delivery, but it preserves the one-time promise instead of approximating it.
The adapter targets S3-compatible APIs through Bun's native S3 support. MinIO runs locally, while R2 or S3 can back production without vendor-specific domain logic.
Trade-off: Fewer vendor-specific optimizations up front, but lower lock-in and simpler portability.
Delayed jobs are necessary but not sufficient. A recurring reconciler repairs missed expirations, orphaned objects, and metadata-storage drift instead of hoping queues never miss.
Trade-off: Adds scheduler and anomaly-management overhead, but keeps correctness from depending on one happy-path job execution.
Anonymous upload raises moderation risk quickly. Files become hidden after a configurable report threshold so the public surface reacts before an admin can manually review every case.
Trade-off: False positives remain possible, so restore is a first-class admin action rather than an afterthought.
The web serves UI, the API owns domain mutations, and the worker owns asynchronous lifecycle work. That keeps the system understandable as it grows.
Trade-off: Operationally heavier than a single process, but much harder to entangle by accident.
These are explicit limitations of the shipped system, not fine print. They explain why the roadmap looks the way it does and prevent the page from promising more than the product actually delivers today.
The initial upload path prioritizes observability and simpler consistency handling over the lower bandwidth cost of direct presigned uploads.
Only supported MIME types are previewable, and one-time files never expose preview because that would undermine single-consumption semantics.
Auto-hide is threshold-based and operated by one admin. It is designed for fast containment in a solo project, not for enterprise review workflows.
anonshare is intentionally narrower than consumer file-sharing suites. The point of v1 is to make trade-offs explicit, not to imitate every adjacent product feature.
These exclusions are scoping decisions, not accidental omissions. They keep the first version coherent instead of turning it into a grab-bag of adjacent features.
These are informed directions after v1, not promises. Each one changes cost, consistency, or product complexity, so the impact matters as much as the idea itself.