{"id":51088691,"url":"https://github.com/bit-web24/bittuly","last_synced_at":"2026-06-23T23:32:46.014Z","repository":{"id":362131077,"uuid":"1256791185","full_name":"bit-web24/bittuly","owner":"bit-web24","description":"Distibuted URL Shortener","archived":false,"fork":false,"pushed_at":"2026-06-18T10:34:20.000Z","size":3539,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-18T12:24:36.403Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bit-web24.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-02T05:06:54.000Z","updated_at":"2026-06-15T20:31:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bit-web24/bittuly","commit_stats":null,"previous_names":["bit-web24/bittuly"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bit-web24/bittuly","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bit-web24%2Fbittuly","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bit-web24%2Fbittuly/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bit-web24%2Fbittuly/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bit-web24%2Fbittuly/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bit-web24","download_url":"https://codeload.github.com/bit-web24/bittuly/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bit-web24%2Fbittuly/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34711176,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-23T23:32:44.938Z","updated_at":"2026-06-23T23:32:46.006Z","avatar_url":"https://github.com/bit-web24.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bittuly — Distributed URL Shortener\n\nBittuly 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.\n\n---\n\n## 🏗️ System Design\n\n```mermaid\ngraph TB\n    %% ── Actors ──────────────────────────────────────────────\n    Browser([\"🌐 Browser\"])\n    GHCR[(\"📦 GHCR\\nghcr.io\")]\n\n    %% ── Edge ────────────────────────────────────────────────\n    subgraph EDGE [\"🛡️  Edge  —  NGINX  :8000\"]\n        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\"]\n    end\n\n    %% ── Services ────────────────────────────────────────────\n    subgraph SVC [\"⚙️  Services  —  Kubernetes / bittuly ns\"]\n        direction LR\n\n        FE[\"🖥️  Frontend\\nReact 18 · Vite · TypeScript\\n━━━━━━━━━━━━━━━━\\nreplicas: 1\\nCPU: 50m → 200m\"]\n\n        subgraph AUTH [\"auth-service  :3001\"]\n            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\"]\n        end\n\n        subgraph URL [\"url-service  :3002\"]\n            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\"]\n        end\n\n        subgraph CONS [\"consumer-service  (worker)\"]\n            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\"]\n        end\n    end\n\n    %% ── Cache ───────────────────────────────────────────────\n    subgraph CACHE [\"⚡  Three-Tier Redirect Cache\"]\n        direction LR\n        L1[\"L1 · Moka\\nin-process\\n━━━━━━━━━\\nTTL: 3s\\nCap: 1M entries\\nSingleflight\\n(Thundering Herd\\nprotection)\"]\n        L2[\"L2 · Redis  :6379\\ndistributed\\n━━━━━━━━━\\nKey: {short_code}\\nSET / SETEX / DEL\\nmaxmem: 256 MB\\npolicy: allkeys-lru\"]\n        L3[\"L3 · Postgres\\nbittuly_urls\\n━━━━━━━━━\\nSELECT original_url\\nWHERE short_code=$1\"]\n        L1 --\u003e|\"miss\"| L2\n        L2 --\u003e|\"miss\"| L3\n    end\n\n    %% ── Databases ───────────────────────────────────────────\n    PGA[(\"🐘 pg-auth  :5432\\nbittuly_auth\\n━━━━━━━━━\\ntable: users\\nid · username\\nemail · bcrypt(pw)\\ncreated_at\")]\n    PGU[(\"🐘 pg-urls  :5433\\nbittuly_urls\\n━━━━━━━━━\\ntable: urls\\nurl_id BIGSERIAL\\nshort_code base62\\noriginal_url\\nclick_count · expires_at\")]\n\n    %% ── Messaging ───────────────────────────────────────────\n    subgraph MQ [\"📨  RabbitMQ  :5672  —  At-Least-Once\"]\n        direction TB\n        CEQ[\"click_events_queue\\ndurable\"]\n        UDQ[\"user_deleted_queue\\ndurable\"]\n        RTQ[\"retry queues\\n3s · 9s · 27s\\n(TTL + DLX)\"]\n        DLQ[\"user_deleted_dlq\\n(dead letter)\"]\n        UDQ --\u003e|\"failure\\nx-retry-count\"| RTQ\n        RTQ --\u003e|\"TTL expire\\nDLX re-route\"| UDQ\n        UDQ --\u003e|\"≥ 3 retries\"| DLQ\n    end\n\n    %% ── Observability ───────────────────────────────────────\n    subgraph OBS [\"🔭  Observability\"]\n        direction LR\n        PROM[\"Prometheus  :9090\\nscrape every 5s\\nhttp_requests_total\\nhttp_request_duration\\nlinks_shortened\\ncache_hits · cache_misses\"]\n        GF[\"Grafana  :3000\\nBittuly Live Traffic\\nRPS · Latency\\nCache Hits/Misses\"]\n        JAE[\"Jaeger  :16686\\nOTLP/gRPC  :4317\\nW3C traceparent\\nacross HTTP + AMQP\"]\n        PROM --\u003e GF\n    end\n\n    %% ── JWT ─────────────────────────────────────────────────\n    subgraph JWTBOX [\"🔐  JWT  —  HS256  HttpOnly Cookies\"]\n        direction LR\n        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)\"]\n    end\n\n    %% ── CI/CD ───────────────────────────────────────────────\n    subgraph CICD [\"♾️  CI/CD  —  GitHub Actions\"]\n        direction LR\n        CI[\"CI\\nfmt · clippy · audit\\ncargo test × 2\\nnpm typecheck\"]\n        CD[\"CD  (master only)\\ndocker build × 4\\npush ghcr.io/{svc}:{sha}\"]\n        CI --\u003e CD\n    end\n\n    %% ── Edges ───────────────────────────────────────────────\n    Browser --\u003e|\":8000\"| GW\n    GW --\u003e FE \u0026 AS \u0026 US\n    CICD --\u003e GHCR --\u003e|\"imagePull\"| SVC\n\n    AS --\u003e|\"READ/WRITE\"| PGA\n    AS --\u003e|\"publish {user_id}\"| UDQ\n\n    US --\u003e|\"READ/WRITE\"| PGU\n    US --\u003e|\"L1 get_with\"| L1\n    L3 -.-\u003e|\"sqlx\"| PGU\n    US --\u003e|\"tokio::spawn\\npublish short_code\"| CEQ\n\n    CEQ --\u003e|\"consume\"| CS\n    UDQ --\u003e|\"consume\"| CS\n    CS --\u003e|\"batch UPDATE\"| PGU\n    CS --\u003e|\"DEL key\"| L2\n\n    PROM --\u003e|\"scrape\"| AS \u0026 US\n    AS \u0026 US \u0026 CS --\u003e|\"OTLP/gRPC\"| JAE\n```\n\n```mermaid\ngraph TB\n    %% ─── External Clients ───────────────────────────────────────────────────\n    Client([\"👤 Client\\n(Browser / API)\"])\n\n    %% ─── CI/CD Pipeline ─────────────────────────────────────────────────────\n    subgraph CICD [\"☁️ CI/CD  —  GitHub Actions\"]\n        direction LR\n        CI[\"🔎 CI Pipeline\\ncargo fmt · clippy · cargo audit\\ncargo test -p auth-service\\ncargo test -p url-service\\nnpm typecheck + build\"]\n        CD[\"📦 CD Pipeline\\ndocker build × 4 images\\npush ghcr.io/{owner}/bittuly-{svc}:{sha}\\n+ :latest tag\"]\n        CI --\u003e CD\n    end\n    GHCR[(\"📦 GHCR\\nghcr.io\\nauth-service:{sha}\\nurl-service:{sha}\\nconsumer-service:{sha}\\nfrontend-service:{sha}\")]\n    CD --\u003e GHCR\n\n    %% ─── Kubernetes Cluster ──────────────────────────────────────────────────\n    subgraph K8S [\"⚓ Kubernetes Cluster  —  Namespace: bittuly\"]\n        direction TB\n\n        subgraph Ingress [\"🔀 NGINX Ingress Controller\"]\n            IG[\"nginx-ingress\\nlimit-rps: 2000\\n/api/auth → auth-service:3001\\n/api/urls → url-service:3002\\n/ → frontend-service:80\"]\n        end\n\n        subgraph Gateway [\"🛡️ NGINX API Gateway  (port 8000)\"]\n            direction TB\n            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\"]\n        end\n\n        subgraph Services [\"⚙️ Microservices\"]\n            direction LR\n\n            subgraph AuthSvc [\"auth-service  (port 3001)\"]\n                direction TB\n                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)\"]\n                ASRB[\"📤 Publishes\\nuser_deleted_queue\\npayload: { user_id: UUID }\\nW3C traceparent in AMQP headers\"]\n            end\n\n            subgraph UrlSvc [\"url-service  (port 3002)\"]\n                direction TB\n                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)\"]\n                USRB[\"📤 Publishes\\nclick_events_queue\\npayload: raw UTF-8 short_code bytes\\nfire-and-forget via tokio::spawn\\nW3C traceparent in AMQP headers\"]\n            end\n\n            subgraph ConsSvc [\"consumer-service  (no HTTP server)\"]\n                direction TB\n                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\"]\n            end\n\n            subgraph FeSvc [\"frontend-service  (port 80 / dev :5173)\"]\n                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\"]\n            end\n        end\n\n        subgraph DataLayer [\"🗄️ Data Layer\"]\n            direction LR\n\n            subgraph Caching [\"⚡ Three-Tier Redirect Cache\"]\n                direction TB\n                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)\"]\n                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)\"]\n                L3[\"L3 — PostgreSQL (source of truth)\\nQuery: SELECT original_url, expires_at\\nFROM urls WHERE short_code = $1\"]\n                L1 --\u003e L2\n                L2 --\u003e L3\n            end\n\n            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\")]\n\n            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\")]\n\n            RD[(\"🔴 Redis 8\\nport 6379\\nL2 redirect cache\\nURL expiry-aware TTL\")]\n        end\n\n        subgraph Messaging [\"📨 RabbitMQ 4 — Event Bus (At-Least-Once)\"]\n            direction TB\n            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)\"]\n        end\n\n        subgraph Observability [\"🔭 Observability Stack\"]\n            direction LR\n            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\"]\n\n            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}\"]\n\n            GF[\"Grafana 11.6\\nUI: :3000  (admin/admin)\\nDashboard:\\n  Bittuly Live Traffic\\n  RPS · Latency\\n  Cache Hits / Misses\\n  Business Metrics\"]\n\n            PROM --\u003e GF\n        end\n\n        subgraph JWT [\"🔐 JWT Token System (HS256 / HttpOnly Cookies)\"]\n            direction TB\n            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\"]\n        end\n    end\n\n    %% ─── Connections ─────────────────────────────────────────────────────────\n    Client --\u003e|\"HTTPS / HTTP\"| CICD\n    Client --\u003e|\"HTTP :8000\"| GW\n    GW --\u003e IG\n    IG --\u003e AS\n    IG --\u003e US\n    IG --\u003e FE\n    AS --\u003e|\"bcrypt + sqlx\"| PGA\n    AS --\u003e ASRB\n    ASRB --\u003e|\"AMQP publish\"| RMQ\n    US --\u003e|\"sqlx\"| PGU\n    US --\u003e|\"Moka L1\\nget_with singleflight\"| L1\n    L1 --\u003e|\"cache miss\"| L2\n    L2 --\u003e|\"Redis miss\"| L3\n    L3 --\u003e|\"sqlx\"| PGU\n    US --\u003e USRB\n    USRB --\u003e|\"tokio::spawn\\nAMQP publish\"| RMQ\n    RMQ --\u003e|\"consume click_events_queue\"| CS\n    RMQ --\u003e|\"consume user_deleted_queue\"| CS\n    CS --\u003e|\"batch UPDATE click_count\"| PGU\n    CS --\u003e|\"DEL {short_code}\"| RD\n    L2 -.-\u003e|\"backed by\"| RD\n    AS --\u003e|\"OTLP/gRPC\"| JAE\n    US --\u003e|\"OTLP/gRPC\"| JAE\n    CS --\u003e|\"OTLP/gRPC\"| JAE\n    PROM --\u003e|\"scrape :3001/metrics\"| AS\n    PROM --\u003e|\"scrape :3002/metrics\"| US\n    GHCR --\u003e|\"imagePullSecret\"| K8S\n```\n\n---\n\n## 🔁 Key Request Flows\n\n### Flow 1 — URL Redirect (Hot Path, p99 \u003c 4ms)\n\n```\nClient ──► NGINX (redirect_limit 20rps)\n         ──► url-service GET /{short_code}\n               ├─ L1 HIT  → Moka cache (singleflight, ~0.01ms) → HTTP 307\n               ├─ L2 HIT  → Redis GET {code} (~0.3ms) → HTTP 307\n               └─ L3 MISS → PostgreSQL SELECT → populate Redis + Moka → HTTP 307\n                                ↓ (fire-and-forget, non-blocking)\n                           tokio::spawn → publish short_code to click_events_queue\n```\n\n### Flow 2 — User Signup (Two-Phase OTP)\n\n```\nClient ──► POST /api/auth/signup\n           │  validate payload (validator crate)\n           │  generate 6-digit OTP\n           │  bcrypt(otp) + bcrypt(password)\n           │  create pending_token JWT (10 min TTL) carrying { email, username, bcrypt(pw), bcrypt(otp) }\n           │  send OTP email via SMTP (lettre) [dev: print to console]\n           └─ return { pending_token }  [no DB write yet]\n\nClient ──► POST /api/auth/verify-otp  { pending_token, otp }\n           │  decode pending_token JWT\n           │  bcrypt verify otp\n           │  INSERT INTO users (...)  [first DB write]\n           │  generate access_token (15m) + refresh_token (30d)\n           └─ set HttpOnly cookies + return user\n```\n\n### Flow 3 — User Deletion Cascade\n\n```\nClient ──► DELETE /api/auth/{user_id}\n           │  DELETE FROM users WHERE id = $1\n           │  publish { user_id } to user_deleted_queue (AMQP, W3C traceparent injected)\n           └─ HTTP 204 immediately returned\n\n[async] consumer-service ◄── user_deleted_queue\n           │  DELETE FROM urls WHERE user_id = $1 RETURNING short_code\n           │  for each short_code: DEL url:{short_code} from Redis\n           └─ basic_ack\n                ↓ on failure (DB down)\n           retry_count 0 → user_deleted_retry_3s  (TTL 3s → DLX back to queue)\n           retry_count 1 → user_deleted_retry_9s  (TTL 9s → DLX back to queue)\n           retry_count 2 → user_deleted_retry_27s (TTL 27s → DLX back to queue)\n           retry_count ≥ 3 → user_deleted_dlq (permanent dead letter)\n```\n\n### Flow 4 — Click Analytics (Eventually Consistent, Batched)\n\n```\n[background] consumer-service ◄── click_events_queue (continuous stream)\n           │  accumulate in HashMap\u003cshort_code, delta_count\u003e\n           │\n           ├─ trigger: total_clicks ≥ 17\n           └─ trigger: tokio::interval every 30s\n                ↓\n           UPDATE urls SET click_count = click_count + delta\n           FROM (SELECT unnest($1::text[]) AS code, unnest($2::bigint[]) AS delta) d\n           WHERE urls.short_code = d.code\n                ↓ success               ↓ failure\n           basic_ack(multiple=true)  basic_nack(requeue=true)\n           clear HashMap             exponential backoff (3^n seconds)\n```\n\n---\n\n## 📨 RabbitMQ Queue Topology\n\n```mermaid\ngraph LR\n    AS_PUB[\"auth-service\\nDELETE /api/auth/{id}\"]\n    US_PUB[\"url-service\\nGET /{short_code}\"]\n\n    UDQ[\"user_deleted_queue\\ndurable\"]\n    CEQ[\"click_events_queue\\ndurable\"]\n\n    R3[\"user_deleted_retry_3s\\nTTL: 3 000ms\\nDLX → user_deleted_queue\"]\n    R9[\"user_deleted_retry_9s\\nTTL: 9 000ms\\nDLX → user_deleted_queue\"]\n    R27[\"user_deleted_retry_27s\\nTTL: 27 000ms\\nDLX → user_deleted_queue\"]\n    DLQ[\"user_deleted_dlq\\nDead Letter Queue\\n(permanent)\"]\n\n    CS[\"consumer-service\"]\n\n    AS_PUB --\u003e|\"publish\\n{user_id}\"| UDQ\n    US_PUB --\u003e|\"publish\\nshort_code\"| CEQ\n    UDQ --\u003e|\"consume\"| CS\n    CEQ --\u003e|\"consume\"| CS\n\n    CS --\u003e|\"retry_count=0\\nnack\"| R3\n    CS --\u003e|\"retry_count=1\\nnack\"| R9\n    CS --\u003e|\"retry_count=2\\nnack\"| R27\n    CS --\u003e|\"retry_count≥3\\nnack\"| DLQ\n    R3 --\u003e|\"TTL expire\\nDLX re-route\"| UDQ\n    R9 --\u003e|\"TTL expire\\nDLX re-route\"| UDQ\n    R27 --\u003e|\"TTL expire\\nDLX re-route\"| UDQ\n```\n\n---\n\n## ⚡ Three-Tier Caching Strategy (Redirect Hot Path)\n\n```mermaid\nflowchart LR\n    REQ[\"GET /{short_code}\"]\n\n    subgraph L1 [\"L1 — Moka (in-process)\"]\n        M[\"Capacity: 1 000 000 entries\\nTTL: 3 seconds\\nSingleflight coalescing\\n→ Thundering Herd protection\\n~0.01ms\"]\n    end\n\n    subgraph L2 [\"L2 — Redis (distributed)\"]\n        R[\"Auto-reconnect ConnectionManager\\nmaxmemory 256MB · allkeys-lru\\nExpiry-aware TTL (SETEX)\\n~0.3ms\"]\n    end\n\n    subgraph L3 [\"L3 — PostgreSQL (source of truth)\"]\n        DB[\"bittuly_urls DB\\n~2-5ms\"]\n    end\n\n    REQ --\u003e M\n    M --\u003e|\"HIT → 307\"| RESP[\"HTTP 307 Redirect\"]\n    M --\u003e|\"MISS\"| R\n    R --\u003e|\"HIT → populate L1 → 307\"| RESP\n    R --\u003e|\"MISS\"| DB\n    DB --\u003e|\"populate L2 + L1 → 307\"| RESP\n```\n\n---\n\n## 🚀 Getting Started\n\n### 1. Start Infrastructure\n\n```bash\ndocker compose up -d\n```\n\nStarts: `postgres-auth` (5432) · `postgres-urls` (5433) · `redis` (6379) · `rabbitmq` (5672/15672) · `jaeger` (16686) · `prometheus` (9090) · `grafana` (3000) · `nginx` (8000)\n\n### 2. Configure Environment\n\nCopy `.env.example` to `.env`. Set `MODE=development` to print OTPs to console instead of sending emails.\n\n```bash\ncp .env.example .env\n```\n\n### 3. Run Services\n\n```bash\ncargo dev-auth      # Terminal 1 — auth-service :3001\ncargo dev-urls      # Terminal 2 — url-service  :3002\ncargo dev-consumer  # Terminal 3 — consumer-service\ncd web \u0026\u0026 npm run dev   # Terminal 4 — frontend :5173\n```\n\nAccess via NGINX gateway: `http://localhost:8000`\n\n### 4. Run Tests\n\n```bash\n./scripts/test.sh           # all services\n./scripts/test.sh auth      # auth-service only\n./scripts/test.sh url       # url-service only\n```\n\n---\n\n## ⚓ Kubernetes Local Deployment\n\n### Deploy\n\n```bash\n./scripts/k8s.sh up     # create kind cluster + load images + apply manifests\n./scripts/k8s.sh down   # destroy cluster\n```\n\n### Access\n\n| Service | URL |\n|---|---|\n| App + API Gateway | `http://localhost:8000` |\n| RabbitMQ UI | `kubectl port-forward -n bittuly svc/rabbitmq 15672:15672` |\n| Jaeger UI | `kubectl port-forward -n bittuly svc/jaeger 16686:16686` |\n\n### K8s Resource Summary\n\n| Workload | Replicas | CPU | Memory | HPA |\n|---|---|---|---|---|\n| auth-service | 1 | 50m → 500m | 64Mi → 256Mi | — |\n| url-service | 2 | 100m → 1000m | 128Mi → 512Mi | min 2, max 5, CPU 60% |\n| consumer-service | 1 | 50m → 500m | 64Mi → 256Mi | — |\n| frontend-service | 1 | 50m → 200m | 64Mi → 128Mi | — |\n\n---\n\n## 🛡️ CI/CD Pipeline\n\n```mermaid\nflowchart LR\n    GH[\"git push / PR\"]\n    subgraph CI [\"CI  (on: push PR → dev master)\"]\n        direction TB\n        LINT[\"rust-lint\\ncargo fmt --check\\ncargo clippy -D warnings\\ncargo audit\"]\n        TA[\"test-auth\\nspins up postgres:17\\ncargo test -p auth-service\"]\n        TU[\"test-url\\nspins up postgres:17 + redis:7\\ncargo test -p url-service\"]\n        FE2[\"frontend\\nnpm ci\\nnpm run typecheck\\nnpm run build\"]\n        DB2[\"docker-build\\nbuildx smoke build × 4\\npush: false\"]\n        LINT --\u003e TA \u0026 TU\n        TA \u0026 TU --\u003e DB2\n    end\n    subgraph CD [\"CD  (on: push → master only)\"]\n        direction TB\n        PUSH[\"build-and-push\\ndocker buildx × 4 images\\nghcr.io/{owner}/bittuly-{svc}:{sha}\\nghcr.io/{owner}/bittuly-{svc}:latest\\nGHA layer cache\"]\n        DEPLOY[\"deploy (commented — pending k8s target)\\nkubectl set image\\nkubectl rollout status --timeout 120s\"]\n        PUSH -.-\u003e|\"future\"| DEPLOY\n    end\n    GH --\u003e CI\n    CI --\u003e CD\n    CD --\u003e GHCR2[(\"ghcr.io\")]\n```\n\n---\n\n## 📊 Observability Endpoints\n\n| Tool | URL | Purpose |\n|---|---|---|\n| NGINX Gateway | `http://localhost:8000` | Main entry point |\n| Grafana | `http://localhost:3000` (admin/admin) | Live traffic dashboards |\n| Prometheus | `http://localhost:9090` | Raw metrics + query |\n| Jaeger | `http://localhost:16686` | Distributed traces |\n| RabbitMQ UI | `http://localhost:15672` (bittu/bittu) | Queue management |\n\n\u003e **Note**: `/api/*/metrics` endpoints are blocked with `403` at the NGINX gateway level. Prometheus scrapes services directly on their internal ports.\n\n---\n\n## 🛠️ Useful Commands\n\n```bash\n# Wipe databases and restart fresh\ndocker compose down -v \u0026\u0026 docker compose up -d\n\n# Run full local CI checks (same as GitHub Actions)\n./scripts/check.sh\n\n# API endpoint smoke tests\n./scripts/api-test.sh\n\n# Load test (k6 via kubectl inside cluster)\n./scripts/load-test.sh\n\n# Production build\ncargo build --release\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbit-web24%2Fbittuly","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbit-web24%2Fbittuly","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbit-web24%2Fbittuly/lists"}