Warning
This project is under active development. APIs, schema, and features may change without notice. Not production-ready.
ByteBay is a modern, high-performance cloud storage application (a Google Drive clone). Built as a Turborepo monorepo with a React frontend and an Express backend, powered entirely by the Bun runtime.
| Layer | Technology |
|---|---|
| Runtime & Package Manager | Bun |
| Monorepo Orchestration | Turborepo |
| Frontend | React 19 + Vite 7 (SWC) + TypeScript |
| Docs Site | Astro 6 + Starlight |
| Routing | TanStack Router v1 (file-based) |
| Server State | TanStack Query v5 |
| Styling | Tailwind CSS v4 + Wabi-Sabi theme |
| Backend | Express 5 + TypeScript + Bun |
| Database | PostgreSQL + Drizzle ORM |
| Authentication | Better Auth |
| File Storage | AWS S3 / RustFS (local dev) |
| Validation | Zod v4 |
| API Testing | Bruno (@usebruno/cli) |
| Unit & Integration Tests | Bun test runner + supertest |
| Code Quality | ESLint 9 (Flat Config), Prettier, Husky, lint-staged |
byte-bay/
├── apps/
│ ├── api/ — Express 5 backend (:3000)
│ │ └── src/
│ │ ├── config/ — DB, auth, S3 config
│ │ ├── middlewares/
│ │ ├── utils/
│ │ │ ├── errors.ts — HttpError base + NotFoundError, GoneError, ConflictError
│ │ │ └── session.ts — getSession(res) helper for controllers
│ │ └── modules/ — feature modules (auth, users, files, folders, …)
│ ├── client/ — React 19 + Vite frontend (:5173)
│ │ └── src/
│ │ ├── lib/ — auth client, API helpers, TanStack Query client, tokens, focus-trap
│ │ ├── routes/ — file-based TanStack Router routes
│ │ └── components/ — reusable UI components (Wabi-Sabi themed)
│ └── home/ — Astro + Starlight docs site (:4321 dev, :8080 docker)
└── packages/
├── eslint-config/ — shared ESLint flat configs
├── typescript-config/ — shared tsconfig bases
└── ui/ — shared React component library
Install Bun:
curl -fsSL https://bun.sh/install | bashbun installcp apps/api/.env.example apps/api/.envFill in apps/api/.env:
PORT=3000
DATABASE_URL=postgres://postgres:postgres@localhost:5432/bytebay
BETTER_AUTH_SECRET= # generate: openssl rand -base64 32
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:5173
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
S3_BUCKET=
# For local dev with RustFS:
AWS_ENDPOINT_URL=http://localhost:9000
AWS_PRESIGN_ENDPOINT_URL=http://localhost:9000
PRESIGNED_URL_TTL=3600Starts postgres, RustFS, API, client, and docs together:
docker compose up --buildMigrations — after first boot, run:
cd apps/api && DATABASE_URL=postgres://postgres:postgres@localhost:5432/bytebay bun run db:migrateRustFS — create the bytebay bucket on first boot. Two options:
- Web console at
http://localhost:9001(credentials:rustfs/rustfs123) - One-shot script:
bun -e " import { S3Client, CreateBucketCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ endpoint: 'http://localhost:9000', region: 'us-east-1', credentials: { accessKeyId: 'rustfs', secretAccessKey: 'rustfs123' }, forcePathStyle: true }); await s3.send(new CreateBucketCommand({ Bucket: 'bytebay' }));"
The bucket lives in the rustfs_data docker volume — only re-create after docker compose down -v.
| Service | URL |
|---|---|
| Client | http://localhost:80 |
| Docs | http://localhost:8080 |
| API | http://localhost:3000 |
| PostgreSQL | localhost:5432 |
| RustFS S3 | http://localhost:9000 |
| RustFS UI | http://localhost:9001 |
Start a local Postgres instance, apply migrations, then:
bun run db:migrate # from apps/api
bun dev # from repo root- Client:
http://localhost:5173 - API:
http://localhost:3000 - Docs:
http://localhost:4321 - Vite proxies all
/api/*requests to the API automatically.
From the repo root:
| Command | Description |
|---|---|
bun dev |
Start all apps in watch mode |
bun build |
Build all apps and packages |
bun lint |
Run ESLint across the monorepo |
bun format |
Format all files with Prettier |
bun format:check |
Check formatting without writing |
bun check-types |
Type-check all packages |
bun run test |
Run unit + integration tests (needs Docker) |
bun run test:unit |
Unit tests only — no Docker required |
From apps/api:
| Command | Description |
|---|---|
bun run test:unit |
Unit tests only |
bun run test:integration |
Integration tests only |
bun run test:coverage |
Tests with coverage report |
bun run db:generate |
Generate Drizzle migration files |
bun run db:migrate |
Apply migrations to the database |
bun run db:studio |
Open Drizzle Studio |
Never run bare
bun testfrom the repo root orapps/api. It triggers a Bun 1.3.13mock.moduleisolation bug and causes ~16 unit test failures. Usebun run testorbun run test:unitinstead.
The API follows a Feature-Based (Domain-Driven) architecture. Every module under src/modules/[feature]/ contains exactly five files:
| File | Responsibility |
|---|---|
[feature].schema.ts |
Drizzle table definitions |
[feature].interfaces.ts |
Types inferred from schema + DTOs |
[feature].routes.ts |
Route declarations + middleware only |
[feature].controller.ts |
HTTP layer — delegates to service |
[feature].service.ts |
Business logic + DB queries |
Scaffolded modules: auth, users, folders, files, file-versions, shared-links, audit-events.
Shared utilities in src/utils/:
errors.ts—HttpErrorbase class + genericNotFoundError,GoneError,ConflictErrorsession.ts—getSession(res)helper; use in controllers instead of inlineres.localscast
Request bodies validated with Zod v4 via shared validate middleware. Responses validated client-side via Zod schemas in src/lib/api.ts (axios + Zod).
router.post('/sign-up', validate(signUpSchema), authController.signUp);Each module's [feature].interfaces.ts owns its Zod schemas and inferred input types.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/sign-up |
Public | Register — validated by Zod |
| POST | /api/auth/sign-in/email |
Public | Sign in (Better Auth native) |
| POST | /api/auth/sign-out |
Required | Sign out |
| GET | /api/auth/me |
Required | Current session |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/folders |
Required | List folders; optional ?parentFolderId= filter |
| POST | /api/folders |
Required | Create folder |
| GET | /api/folders/:id |
Required | Get single folder |
| PATCH | /api/folders/:id |
Required | Rename or move folder; body: { name? } or { parentFolderId? } (null = root); rejects circular moves |
| DELETE | /api/folders/:id |
Required | Soft delete (trash) — cascades to child folders and their files |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/files |
Required | Initiate upload — returns presigned PUT URL; mimeType validated against allowlist |
| GET | /api/files |
Required | List files (excludes trashed) |
| GET | /api/files/:id |
Required | Get single file |
| GET | /api/files/:id/download-url |
Required | Presigned download URL |
| PATCH | /api/files/:id |
Required | Move file; body: { folderId: string | null } (null = root) |
| DELETE | /api/files/:id |
Required | Soft delete (trash) |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/shared-links |
Required | Create shared link for a file |
| GET | /api/shared-links/:token |
Public | Resolve token → file info + download URL |
| DELETE | /api/shared-links/:id |
Required | Revoke link |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/audit-events |
Required | List caller's events; optional ?action=, ?fileId=, ?from=, ?to=, ?limit=, ?offset= |
The collection lives in bruno/. Run with Bruno CLI:
cd bruno && bunx bru run --env local # full collection
cd bruno && bunx bru run 02-folders --env local # single folderFolder order matters — Bruno executes folders alphabetically:
| Folder | Contents | Vars produced |
|---|---|---|
01-auth/ |
sign-up, sign-in, me | sessionCookie, testEmail |
02-folders/ |
create, list, get, rename, create-child, move, trash | folderId, childFolderId |
03-files/ |
initiate-upload, list, get, download-url, move | fileId, uploadUrl |
04-shared-links/ |
create, resolve (public — no auth), revoke | sharedLinkToken, sharedLinkId |
05-audit-events/ |
list | — |
06-teardown/ |
trash-file, sign-out | — |
Trash deferred to 06-teardown/ so 04-shared-links/ can resolve a live file. Sign-out last — doesn't invalidate session mid-run.
Secrets: bruno/.env (gitignored) holds TEST_PASSWORD. Copy from bruno/.env.example.
Unit tests live alongside each module. Integration tests use testcontainers to spin up real Postgres and MinIO containers — no mocks, real SQL, real presigned URLs, real Better Auth sessions.
Requires Docker running.
# From repo root (preferred):
bun run test # unit + integration (needs Docker)
bun run test:unit # unit only
# From apps/api:
bun run test:unit # src/modules/ only
bun run test:integration # src/integration/ only (starts containers)Do not run bare
bun test— see warning in Commands section above.
Pre-commit (every commit):
bun lint— ESLint across the repolint-staged— Prettier on staged filesbun check-types— TypeScript across all packages
Pre-push (before push):
bun format:check— verify all files are formattedcd apps/api && bun run test:unit && bun test src/integration/— full test suite
Any failing check aborts the operation.