https://github.com/bit-web24/bittuly
Distibuted URL Shortener
https://github.com/bit-web24/bittuly
Last synced: 6 days ago
JSON representation
Distibuted URL Shortener
- Host: GitHub
- URL: https://github.com/bit-web24/bittuly
- Owner: bit-web24
- License: gpl-3.0
- Created: 2026-06-02T05:06:54.000Z (28 days ago)
- Default Branch: master
- Last Pushed: 2026-06-18T10:34:20.000Z (12 days ago)
- Last Synced: 2026-06-18T12:24:36.403Z (12 days ago)
- Language: TypeScript
- Size: 3.38 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Bittuly β Distributed URL Shortener
Bittuly is a production-grade, distributed URL shortener built with **Rust (Axum)** and **React (Vite)**. It uses a microservices architecture with isolated databases per service, a two-tier caching system, asynchronous event-driven analytics, and full observability via OpenTelemetry, Prometheus, and Grafana.
---
## ποΈ System Design
```mermaid
graph TB
%% ββ Actors ββββββββββββββββββββββββββββββββββββββββββββββ
Browser(["π Browser"])
GHCR[("π¦ GHCR\nghcr.io")]
%% ββ Edge ββββββββββββββββββββββββββββββββββββββββββββββββ
subgraph EDGE ["π‘οΈ Edge β NGINX :8000"]
GW["API Gateway\nβββββββββββββββββββββ\n/api/auth/* β auth-service\n/api/urls/* β url-service\n/{short_code} β url-service\n/* β frontend\nβββββββββββββββββββββ\nπ Rate Limits\nlogin/signup 5 req/min burst 5\nredirects 20 req/s burst 20\nπ Security Headers\nX-Frame-Options Β· CSP Β· HSTS\nπ« /api/*/metrics β 403"]
end
%% ββ Services ββββββββββββββββββββββββββββββββββββββββββββ
subgraph SVC ["βοΈ Services β Kubernetes / bittuly ns"]
direction LR
FE["π₯οΈ Frontend\nReact 18 Β· Vite Β· TypeScript\nββββββββββββββββ\nreplicas: 1\nCPU: 50m β 200m"]
subgraph AUTH ["auth-service :3001"]
AS["Rust Β· Axum\nJWT HS256 Β· bcrypt\nβββββββββββββββββββββ\nPOST /signup β OTP email\nPOST /verify-otp β create user\nPOST /login β set cookies\nDEL /{id} β cascade delete\nβββββββββββββββββββββ\nreplicas: 1\nCPU: 50m β 500m\nHealth: GET /health"]
end
subgraph URL ["url-service :3002"]
US["Rust Β· Axum\nJWT HS256 Β· base62\nβββββββββββββββββββββ\nPOST /api/urls β shorten\nGET /api/urls β paginated list\nDEL /api/urls/{id}\nGET /{code} β 307 redirect β‘\nβββββββββββββββββββββ\nreplicas: 2 HPA: 2β5 @ 60% CPU\nCPU: 100m β 1000m\nHealth: GET /health"]
end
subgraph CONS ["consumer-service (worker)"]
CS["Rust Β· Tokio\nNo HTTP server\nβββββββββββββββββββββ\nβ click_events_queue\n batch flush every 30s or 17 clicks\n UPDATE click_count (unnest)\nβ‘ user_deleted_queue\n cascade DEL urls + Redis eviction\n retry 3sβ9sβ27s β DLQ\nβββββββββββββββββββββ\nreplicas: 1\nCPU: 50m β 500m"]
end
end
%% ββ Cache βββββββββββββββββββββββββββββββββββββββββββββββ
subgraph CACHE ["β‘ Three-Tier Redirect Cache"]
direction LR
L1["L1 Β· Moka\nin-process\nβββββββββ\nTTL: 3s\nCap: 1M entries\nSingleflight\n(Thundering Herd\nprotection)"]
L2["L2 Β· Redis :6379\ndistributed\nβββββββββ\nKey: {short_code}\nSET / SETEX / DEL\nmaxmem: 256 MB\npolicy: allkeys-lru"]
L3["L3 Β· Postgres\nbittuly_urls\nβββββββββ\nSELECT original_url\nWHERE short_code=$1"]
L1 -->|"miss"| L2
L2 -->|"miss"| L3
end
%% ββ Databases βββββββββββββββββββββββββββββββββββββββββββ
PGA[("π pg-auth :5432\nbittuly_auth\nβββββββββ\ntable: users\nid Β· username\nemail Β· bcrypt(pw)\ncreated_at")]
PGU[("π pg-urls :5433\nbittuly_urls\nβββββββββ\ntable: urls\nurl_id BIGSERIAL\nshort_code base62\noriginal_url\nclick_count Β· expires_at")]
%% ββ Messaging βββββββββββββββββββββββββββββββββββββββββββ
subgraph MQ ["π¨ RabbitMQ :5672 β At-Least-Once"]
direction TB
CEQ["click_events_queue\ndurable"]
UDQ["user_deleted_queue\ndurable"]
RTQ["retry queues\n3s Β· 9s Β· 27s\n(TTL + DLX)"]
DLQ["user_deleted_dlq\n(dead letter)"]
UDQ -->|"failure\nx-retry-count"| RTQ
RTQ -->|"TTL expire\nDLX re-route"| UDQ
UDQ -->|"β₯ 3 retries"| DLQ
end
%% ββ Observability βββββββββββββββββββββββββββββββββββββββ
subgraph OBS ["π Observability"]
direction LR
PROM["Prometheus :9090\nscrape every 5s\nhttp_requests_total\nhttp_request_duration\nlinks_shortened\ncache_hits Β· cache_misses"]
GF["Grafana :3000\nBittuly Live Traffic\nRPS Β· Latency\nCache Hits/Misses"]
JAE["Jaeger :16686\nOTLP/gRPC :4317\nW3C traceparent\nacross HTTP + AMQP"]
PROM --> GF
end
%% ββ JWT βββββββββββββββββββββββββββββββββββββββββββββββββ
subgraph JWTBOX ["π JWT β HS256 HttpOnly Cookies"]
direction LR
JWTD["access_token 15 min cookie\nrefresh_token 30 days cookie\npending_token 10 min JSON body\n(carries bcrypt pw+otp β no DB row until OTP verified)"]
end
%% ββ CI/CD βββββββββββββββββββββββββββββββββββββββββββββββ
subgraph CICD ["βΎοΈ CI/CD β GitHub Actions"]
direction LR
CI["CI\nfmt Β· clippy Β· audit\ncargo test Γ 2\nnpm typecheck"]
CD["CD (master only)\ndocker build Γ 4\npush ghcr.io/{svc}:{sha}"]
CI --> CD
end
%% ββ Edges βββββββββββββββββββββββββββββββββββββββββββββββ
Browser -->|":8000"| GW
GW --> FE & AS & US
CICD --> GHCR -->|"imagePull"| SVC
AS -->|"READ/WRITE"| PGA
AS -->|"publish {user_id}"| UDQ
US -->|"READ/WRITE"| PGU
US -->|"L1 get_with"| L1
L3 -.->|"sqlx"| PGU
US -->|"tokio::spawn\npublish short_code"| CEQ
CEQ -->|"consume"| CS
UDQ -->|"consume"| CS
CS -->|"batch UPDATE"| PGU
CS -->|"DEL key"| L2
PROM -->|"scrape"| AS & US
AS & US & CS -->|"OTLP/gRPC"| JAE
```
```mermaid
graph TB
%% βββ External Clients βββββββββββββββββββββββββββββββββββββββββββββββββββ
Client(["π€ Client\n(Browser / API)"])
%% βββ CI/CD Pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββ
subgraph CICD ["βοΈ CI/CD β GitHub Actions"]
direction LR
CI["π CI Pipeline\ncargo fmt Β· clippy Β· cargo audit\ncargo test -p auth-service\ncargo test -p url-service\nnpm typecheck + build"]
CD["π¦ CD Pipeline\ndocker build Γ 4 images\npush ghcr.io/{owner}/bittuly-{svc}:{sha}\n+ :latest tag"]
CI --> CD
end
GHCR[("π¦ GHCR\nghcr.io\nauth-service:{sha}\nurl-service:{sha}\nconsumer-service:{sha}\nfrontend-service:{sha}")]
CD --> GHCR
%% βββ Kubernetes Cluster ββββββββββββββββββββββββββββββββββββββββββββββββββ
subgraph K8S ["β Kubernetes Cluster β Namespace: bittuly"]
direction TB
subgraph Ingress ["π NGINX Ingress Controller"]
IG["nginx-ingress\nlimit-rps: 2000\n/api/auth β auth-service:3001\n/api/urls β url-service:3002\n/ β frontend-service:80"]
end
subgraph Gateway ["π‘οΈ NGINX API Gateway (port 8000)"]
direction TB
GW["nginx:1.31.1-alpine\nworker_processes: auto\nworker_connections: 50 000\nmax FD: 200 000\n\nβββ Rate Limiting βββ\nredirect_limit 20 req/s burst 20 200 MB zone\nauth_limit 5 req/min burst 5 50 MB zone\n\nβββ Security Headers βββ\nX-Frame-Options: DENY\nX-Content-Type-Options: nosniff\nX-XSS-Protection: 1; mode=block\nReferrer-Policy: strict-origin-when-cross-origin\nPermissions-Policy: camera=() microphone=() geolocation=()\nContent-Security-Policy: default-src 'self'\n\nβββ Routing βββ\nGET /api/*/metrics β 403 Forbidden\nPOST /api/auth/login|signup β auth :3001 + auth_limit\n/api/auth/* β auth :3001\n/api/urls/* β url :3002\n/{short_code} β url :3002 + redirect_limit\n/ dashboard login signup β frontend :5173"]
end
subgraph Services ["βοΈ Microservices"]
direction LR
subgraph AuthSvc ["auth-service (port 3001)"]
direction TB
AS["Rust Β· Axum Β· HS256 JWT\nβββ HTTP Routes βββ\nPOST /api/auth/signup β OTP email, returns pending_token JWT\nPOST /api/auth/verify-otp β verify OTP, create user, set cookies\nPOST /api/auth/login β bcrypt verify, set cookies\nPOST /api/auth/logout β clear cookies\nGET /api/auth/{user_id} β get user (owner only)\nPUT /api/auth/{user_id} β update user, re-issue JWTs\nDEL /api/auth/{user_id} β delete user, publish to RabbitMQ\nGET /api/auth/health β health check\nGET /api/auth/metrics β Prometheus (blocked by GW)\n\nβββ K8s Resources βββ\nreplicas: 1 CPU: 50mβ500m Mem: 64Miβ256Mi\nliveness: GET /api/auth/health (delay 10s period 15s)\nreadiness: GET /api/auth/health (delay 5s period 5s)"]
ASRB["π€ Publishes\nuser_deleted_queue\npayload: { user_id: UUID }\nW3C traceparent in AMQP headers"]
end
subgraph UrlSvc ["url-service (port 3002)"]
direction TB
US["Rust Β· Axum Β· HS256 JWT\nβββ HTTP Routes βββ\nPOST /api/urls β shorten URL, incr links_shortened metric\nGET /api/urls β cursor-paginated list (default 20 max 100)\nDEL /api/urls/{id} β delete URL, evict Redis + Moka cache\nGET /{short_code} β HTTP 307 redirect (hot path)\nGET /api/urls/health β health check\nGET /api/urls/metrics β Prometheus (blocked by GW)\n\nβββ K8s Resources βββ\nreplicas: 2 CPU: 100mβ1000m Mem: 128Miβ512Mi\nHPA: min=2 max=5 CPU target=60%\nliveness: GET /api/urls/health (delay 10s period 15s)\nreadiness: GET /api/urls/health (delay 5s period 5s)\n\nβββ Short Code Generation βββ\nbase62( url_id: BIGSERIAL )\nINSERT urls β get url_id β UPDATE short_code (in transaction)"]
USRB["π€ Publishes\nclick_events_queue\npayload: raw UTF-8 short_code bytes\nfire-and-forget via tokio::spawn\nW3C traceparent in AMQP headers"]
end
subgraph ConsSvc ["consumer-service (no HTTP server)"]
direction TB
CS["Rust Β· Tokio worker\nβββ Consumers βββ\n\nβ click_events_queue (tag: consumer_service_clicks)\n in-memory HashMap short_codeβcount\n Flush on: size β₯ 17 clicks OR timer every 30s\n SQL: UPDATE urls SET click_count = click_count + delta\n FROM unnest arrays (single batch statement)\n ACK: basic_ack(multiple=true) on success\n NACK: basic_nack(multiple=true, requeue=true) on DB failure\n Retry: exponential backoff 3^n seconds\n\nβ‘ user_deleted_queue (tag: consumer_service_users)\n Extract W3C traceparent from AMQP headers\n DELETE FROM urls WHERE user_id = $1 RETURNING short_code\n DEL {short_code} from Redis (per returned code)\n ACK on success\n Retry: x-retry-count header\n count 0-2 β user_deleted_retry_{3|9|27}s queue (TTL+DLX)\n count β₯ 3 β user_deleted_dlq (Dead Letter Queue)\n Invalid JSON β silent ack-discard\n\nβββ K8s Resources βββ\nreplicas: 1 CPU: 50mβ500m Mem: 64Miβ256Mi"]
end
subgraph FeSvc ["frontend-service (port 80 / dev :5173)"]
FE["React 18 Β· TypeScript Β· Vite Β· React Router v6\nβββ Pages βββ\n/signup Signup (OTP step 1)\n/verify-otp VerifyOtp (OTP step 2)\n/login Login\n/dashboard Dashboard (ProtectedRoute)\n/insights Insights (ProtectedRoute)\n/profile Profile (ProtectedRoute)\n/settings Settings (ProtectedRoute)\n/health ServiceHealth (ProtectedRoute)\n/unavailable Unavailable (fallback)\n\nβββ Auth βββ\nAuthContext + ProtectedRoute\nJWT via HttpOnly cookies (no JS access)\n\nβββ K8s Resources βββ\nreplicas: 1 CPU: 50mβ200m Mem: 64Miβ128Mi"]
end
end
subgraph DataLayer ["ποΈ Data Layer"]
direction LR
subgraph Caching ["β‘ Three-Tier Redirect Cache"]
direction TB
L1["L1 β Moka (in-process)\nType: future::Cache\nCapacity: 1 000 000 entries\nTTL: 3 seconds\nStrategy: Singleflight coalescing\n(Thundering Herd protection)\nKey: short_code β Option(original_url, expires_at)"]
L2["L2 β Redis 8 (distributed)\nConnectionManager (auto-reconnect)\nmaxmemory: 256 MB policy: allkeys-lru\nKey pattern: {short_code}\nSET {code} {url} (permanent)\nSETEX {code} {url} {ttl} (expiring)\nDEL {code} (on delete)"]
L3["L3 β PostgreSQL (source of truth)\nQuery: SELECT original_url, expires_at\nFROM urls WHERE short_code = $1"]
L1 --> L2
L2 --> L3
end
PGA[("π pg-auth\nPostgres 17\nport 5432\nDB: bittuly_auth\n\ntable: users\n id UUID PK\n username TEXT\n email TEXT UNIQUE\n password TEXT (bcrypt)\n created_at TIMESTAMPTZ\n updated_at TIMESTAMPTZ")]
PGU[("π pg-urls\nPostgres 17\nport 5433\nDB: bittuly_urls\n\ntable: urls\n url_id BIGSERIAL PK\n short_code TEXT UNIQUE (base62)\n original_url TEXT\n user_id UUID FK\n click_count BIGINT\n expires_at TIMESTAMPTZ nullable\n created_at TIMESTAMPTZ\n updated_at TIMESTAMPTZ")]
RD[("π΄ Redis 8\nport 6379\nL2 redirect cache\nURL expiry-aware TTL")]
end
subgraph Messaging ["π¨ RabbitMQ 4 β Event Bus (At-Least-Once)"]
direction TB
RMQ["RabbitMQ 4.3.1-management\nport 5672 management: 15672\nAll queues: durable\n\nβββ Queue Topology βββ\nclick_events_queue\n β payload: raw short_code bytes\n ββ consumer-service (batch flush to pg-urls)\n\nuser_deleted_queue ββββββββββββββββββββββββββββββββ\n β payload: { user_id: UUID } β DLX re-route\n β AMQP headers: W3C traceparent β (on TTL expire)\n ββ consumer-service β\n ββ success β basic_ack β\n ββ retry 1 β user_deleted_retry_3s (TTL 3s) β€\n ββ retry 2 β user_deleted_retry_9s (TTL 9s) β€\n ββ retry 3 β user_deleted_retry_27s (TTL 27s)β€\n ββ retry β₯3 β user_deleted_dlq (permanent DLQ)"]
end
subgraph Observability ["π Observability Stack"]
direction LR
JAE["Jaeger 2.18.0\nOTLP/gRPC :4317\nUI: :16686\n\nTraced services:\n auth-service\n url-service\n consumer-service\n\nSpans:\n All HTTP routes (OtelAxumLayer)\n delete_user AMQP publish\n get_original_url AMQP publish\n process_click_batch\n attrs: batch_size, unique_urls\n process_user_deleted_event\n attrs: user_id\n W3C traceparent propagated\n across HTTP + AMQP boundaries"]
PROM["Prometheus v3\nUI: :9090\nScrape interval: 5s\n\nMetrics:\n http_requests_total\n {method, path, status}\n http_request_duration_seconds\n {method, path, status}\n links_shortened (counter)\n cache_hits (counter)\n cache_misses (counter)\n rabbit_mq_events_published\n {queue=click_events_queue}"]
GF["Grafana 11.6\nUI: :3000 (admin/admin)\nDashboard:\n Bittuly Live Traffic\n RPS Β· Latency\n Cache Hits / Misses\n Business Metrics"]
PROM --> GF
end
subgraph JWT ["π JWT Token System (HS256 / HttpOnly Cookies)"]
direction TB
JWTD["access_token cookie TTL 15 min\n { sub: UUID, exp, token_type: access }\n HttpOnly Β· SameSite=Strict Β· Secure (prod)\n\nrefresh_token cookie TTL 30 days\n { sub: UUID, exp, token_type: refresh }\n Silent auto-rotation on expiry\n\npending_token JSON body TTL 10 min\n { email, username, bcrypt(password),\n bcrypt(otp), token_type: pending }\n Never stored in DB until OTP verified\n Two-phase OTP: no user row until verified"]
end
end
%% βββ Connections βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Client -->|"HTTPS / HTTP"| CICD
Client -->|"HTTP :8000"| GW
GW --> IG
IG --> AS
IG --> US
IG --> FE
AS -->|"bcrypt + sqlx"| PGA
AS --> ASRB
ASRB -->|"AMQP publish"| RMQ
US -->|"sqlx"| PGU
US -->|"Moka L1\nget_with singleflight"| L1
L1 -->|"cache miss"| L2
L2 -->|"Redis miss"| L3
L3 -->|"sqlx"| PGU
US --> USRB
USRB -->|"tokio::spawn\nAMQP publish"| RMQ
RMQ -->|"consume click_events_queue"| CS
RMQ -->|"consume user_deleted_queue"| CS
CS -->|"batch UPDATE click_count"| PGU
CS -->|"DEL {short_code}"| RD
L2 -.->|"backed by"| RD
AS -->|"OTLP/gRPC"| JAE
US -->|"OTLP/gRPC"| JAE
CS -->|"OTLP/gRPC"| JAE
PROM -->|"scrape :3001/metrics"| AS
PROM -->|"scrape :3002/metrics"| US
GHCR -->|"imagePullSecret"| K8S
```
---
## π Key Request Flows
### Flow 1 β URL Redirect (Hot Path, p99 < 4ms)
```
Client βββΊ NGINX (redirect_limit 20rps)
βββΊ url-service GET /{short_code}
ββ L1 HIT β Moka cache (singleflight, ~0.01ms) β HTTP 307
ββ L2 HIT β Redis GET {code} (~0.3ms) β HTTP 307
ββ L3 MISS β PostgreSQL SELECT β populate Redis + Moka β HTTP 307
β (fire-and-forget, non-blocking)
tokio::spawn β publish short_code to click_events_queue
```
### Flow 2 β User Signup (Two-Phase OTP)
```
Client βββΊ POST /api/auth/signup
β validate payload (validator crate)
β generate 6-digit OTP
β bcrypt(otp) + bcrypt(password)
β create pending_token JWT (10 min TTL) carrying { email, username, bcrypt(pw), bcrypt(otp) }
β send OTP email via SMTP (lettre) [dev: print to console]
ββ return { pending_token } [no DB write yet]
Client βββΊ POST /api/auth/verify-otp { pending_token, otp }
β decode pending_token JWT
β bcrypt verify otp
β INSERT INTO users (...) [first DB write]
β generate access_token (15m) + refresh_token (30d)
ββ set HttpOnly cookies + return user
```
### Flow 3 β User Deletion Cascade
```
Client βββΊ DELETE /api/auth/{user_id}
β DELETE FROM users WHERE id = $1
β publish { user_id } to user_deleted_queue (AMQP, W3C traceparent injected)
ββ HTTP 204 immediately returned
[async] consumer-service βββ user_deleted_queue
β DELETE FROM urls WHERE user_id = $1 RETURNING short_code
β for each short_code: DEL url:{short_code} from Redis
ββ basic_ack
β on failure (DB down)
retry_count 0 β user_deleted_retry_3s (TTL 3s β DLX back to queue)
retry_count 1 β user_deleted_retry_9s (TTL 9s β DLX back to queue)
retry_count 2 β user_deleted_retry_27s (TTL 27s β DLX back to queue)
retry_count β₯ 3 β user_deleted_dlq (permanent dead letter)
```
### Flow 4 β Click Analytics (Eventually Consistent, Batched)
```
[background] consumer-service βββ click_events_queue (continuous stream)
β accumulate in HashMap
β
ββ trigger: total_clicks β₯ 17
ββ trigger: tokio::interval every 30s
β
UPDATE urls SET click_count = click_count + delta
FROM (SELECT unnest($1::text[]) AS code, unnest($2::bigint[]) AS delta) d
WHERE urls.short_code = d.code
β success β failure
basic_ack(multiple=true) basic_nack(requeue=true)
clear HashMap exponential backoff (3^n seconds)
```
---
## π¨ RabbitMQ Queue Topology
```mermaid
graph LR
AS_PUB["auth-service\nDELETE /api/auth/{id}"]
US_PUB["url-service\nGET /{short_code}"]
UDQ["user_deleted_queue\ndurable"]
CEQ["click_events_queue\ndurable"]
R3["user_deleted_retry_3s\nTTL: 3 000ms\nDLX β user_deleted_queue"]
R9["user_deleted_retry_9s\nTTL: 9 000ms\nDLX β user_deleted_queue"]
R27["user_deleted_retry_27s\nTTL: 27 000ms\nDLX β user_deleted_queue"]
DLQ["user_deleted_dlq\nDead Letter Queue\n(permanent)"]
CS["consumer-service"]
AS_PUB -->|"publish\n{user_id}"| UDQ
US_PUB -->|"publish\nshort_code"| CEQ
UDQ -->|"consume"| CS
CEQ -->|"consume"| CS
CS -->|"retry_count=0\nnack"| R3
CS -->|"retry_count=1\nnack"| R9
CS -->|"retry_count=2\nnack"| R27
CS -->|"retry_countβ₯3\nnack"| DLQ
R3 -->|"TTL expire\nDLX re-route"| UDQ
R9 -->|"TTL expire\nDLX re-route"| UDQ
R27 -->|"TTL expire\nDLX re-route"| UDQ
```
---
## β‘ Three-Tier Caching Strategy (Redirect Hot Path)
```mermaid
flowchart LR
REQ["GET /{short_code}"]
subgraph L1 ["L1 β Moka (in-process)"]
M["Capacity: 1 000 000 entries\nTTL: 3 seconds\nSingleflight coalescing\nβ Thundering Herd protection\n~0.01ms"]
end
subgraph L2 ["L2 β Redis (distributed)"]
R["Auto-reconnect ConnectionManager\nmaxmemory 256MB Β· allkeys-lru\nExpiry-aware TTL (SETEX)\n~0.3ms"]
end
subgraph L3 ["L3 β PostgreSQL (source of truth)"]
DB["bittuly_urls DB\n~2-5ms"]
end
REQ --> M
M -->|"HIT β 307"| RESP["HTTP 307 Redirect"]
M -->|"MISS"| R
R -->|"HIT β populate L1 β 307"| RESP
R -->|"MISS"| DB
DB -->|"populate L2 + L1 β 307"| RESP
```
---
## π Getting Started
### 1. Start Infrastructure
```bash
docker compose up -d
```
Starts: `postgres-auth` (5432) Β· `postgres-urls` (5433) Β· `redis` (6379) Β· `rabbitmq` (5672/15672) Β· `jaeger` (16686) Β· `prometheus` (9090) Β· `grafana` (3000) Β· `nginx` (8000)
### 2. Configure Environment
Copy `.env.example` to `.env`. Set `MODE=development` to print OTPs to console instead of sending emails.
```bash
cp .env.example .env
```
### 3. Run Services
```bash
cargo dev-auth # Terminal 1 β auth-service :3001
cargo dev-urls # Terminal 2 β url-service :3002
cargo dev-consumer # Terminal 3 β consumer-service
cd web && npm run dev # Terminal 4 β frontend :5173
```
Access via NGINX gateway: `http://localhost:8000`
### 4. Run Tests
```bash
./scripts/test.sh # all services
./scripts/test.sh auth # auth-service only
./scripts/test.sh url # url-service only
```
---
## β Kubernetes Local Deployment
### Deploy
```bash
./scripts/k8s.sh up # create kind cluster + load images + apply manifests
./scripts/k8s.sh down # destroy cluster
```
### Access
| Service | URL |
|---|---|
| App + API Gateway | `http://localhost:8000` |
| RabbitMQ UI | `kubectl port-forward -n bittuly svc/rabbitmq 15672:15672` |
| Jaeger UI | `kubectl port-forward -n bittuly svc/jaeger 16686:16686` |
### K8s Resource Summary
| Workload | Replicas | CPU | Memory | HPA |
|---|---|---|---|---|
| auth-service | 1 | 50m β 500m | 64Mi β 256Mi | β |
| url-service | 2 | 100m β 1000m | 128Mi β 512Mi | min 2, max 5, CPU 60% |
| consumer-service | 1 | 50m β 500m | 64Mi β 256Mi | β |
| frontend-service | 1 | 50m β 200m | 64Mi β 128Mi | β |
---
## π‘οΈ CI/CD Pipeline
```mermaid
flowchart LR
GH["git push / PR"]
subgraph CI ["CI (on: push PR β dev master)"]
direction TB
LINT["rust-lint\ncargo fmt --check\ncargo clippy -D warnings\ncargo audit"]
TA["test-auth\nspins up postgres:17\ncargo test -p auth-service"]
TU["test-url\nspins up postgres:17 + redis:7\ncargo test -p url-service"]
FE2["frontend\nnpm ci\nnpm run typecheck\nnpm run build"]
DB2["docker-build\nbuildx smoke build Γ 4\npush: false"]
LINT --> TA & TU
TA & TU --> DB2
end
subgraph CD ["CD (on: push β master only)"]
direction TB
PUSH["build-and-push\ndocker buildx Γ 4 images\nghcr.io/{owner}/bittuly-{svc}:{sha}\nghcr.io/{owner}/bittuly-{svc}:latest\nGHA layer cache"]
DEPLOY["deploy (commented β pending k8s target)\nkubectl set image\nkubectl rollout status --timeout 120s"]
PUSH -.->|"future"| DEPLOY
end
GH --> CI
CI --> CD
CD --> GHCR2[("ghcr.io")]
```
---
## π Observability Endpoints
| Tool | URL | Purpose |
|---|---|---|
| NGINX Gateway | `http://localhost:8000` | Main entry point |
| Grafana | `http://localhost:3000` (admin/admin) | Live traffic dashboards |
| Prometheus | `http://localhost:9090` | Raw metrics + query |
| Jaeger | `http://localhost:16686` | Distributed traces |
| RabbitMQ UI | `http://localhost:15672` (bittu/bittu) | Queue management |
> **Note**: `/api/*/metrics` endpoints are blocked with `403` at the NGINX gateway level. Prometheus scrapes services directly on their internal ports.
---
## π οΈ Useful Commands
```bash
# Wipe databases and restart fresh
docker compose down -v && docker compose up -d
# Run full local CI checks (same as GitHub Actions)
./scripts/check.sh
# API endpoint smoke tests
./scripts/api-test.sh
# Load test (k6 via kubectl inside cluster)
./scripts/load-test.sh
# Production build
cargo build --release
```