Skip to content

Repository files navigation


TGStore

License Stars Issues PRs Welcome
Mermaid Next.js 14 FastAPI Postgres Telegram


TGStore turns a private Telegram channel into an unlimited, free, end-to-end-self-hosted personal cloud. No S3 bills. No vendor lock-in. Just a bot token, a channel, and code you can read in one sitting.


Quick Start   Architecture   User Journey   API


💡 The pitch

Why pay AWS for storage when a Telegram bot will hold your files for free?

Telegram gives every bot a globally-cached CDN with multi-GB file support and no egress fees. TGStore is the thin, opinionated layer that turns a private Telegram channel into a Dropbox-class personal cloud — for one person, on hardware you control, with code you can read in one sitting.

  ┌──────────────┐    multipart     ┌────────────┐  sendDocument   ┌──────────────────┐
  │  Your laptop │ ───────────────▶ │  FastAPI   │ ──────────────▶ │  Telegram CDN    │
  │  (Next.js)   │  Bearer JWT      │  + SQLAlch │  getFile (1h)   │  private channel │
  └──────────────┘ ◀─── stream ──── └────────────┘ ◀──── bytes ─── └──────────────────┘
                                            │
                                            ▼
                                    ┌──────────────┐
                                    │  PostgreSQL  │
                                    │  (metadata)  │
                                    └──────────────┘

What you get

  • 💸 Zero storage cost — Telegram's CDN is the bucket. You only pay for a few KB of metadata per file in Postgres. No S3 bills. No egress fees. No "you've exceeded your free tier" emails.
  • 🛡 The bot token never reaches the browser — Telegram's download URLs embed the bot token and expire in ~1 hour. TGStore regenerates them server-side on every request and proxies bytes through /stream. Even a curious DevTools user can never exfiltrate the token.
  • 🧍 Single-user, self-host, no surprise bills — one admin, one channel, no teams, no sharing ACLs, no per-seat pricing. The auth layer is literally one username + bcrypt password read from env vars.
  • 🧰 Boring tech on purpose — Next.js 14, FastAPI, Postgres 16. No microservices, no message bus, no Kubernetes, no service mesh. docker compose up and you're done in under five minutes.
  • 🔍 Full-text search, live stats, soft-delete, recovery — even though the backend is "just a thin layer," it ships with case-insensitive trigram search, a stacked-bar storage breakdown by MIME type, and a deleted_at column so a fat-finger delete is reversible.
  • 🧪 Tested end-to-end — 6 async integration tests mock the Telegram service layer, so the CI proves the upload pipeline, the 2 GB cap, and the JWT guard without ever talking to api.telegram.org.

What it isn't (so you don't get the wrong idea)

  • ❌ Not a team drive. No sharing, no per-user permissions, no comments.
  • ❌ Not a Telegram client. The bot is the storage engine; you never see your files in the Telegram app (and shouldn't — that would be a privacy footgun).
  • ❌ Not a multi-tenant SaaS. One instance = one user. Run as many as you like, each pointing at its own channel.
  • ❌ Not a replacement for S3 for mission-critical workloads. Telegram can lose access to a file (though it's never happened to the author in 18 months). Treat it as a free personal cloud, not a regulated-archive backend.

🧠 Architecture

The system is three layers, all under your control. The animation below traces a real upload end-to-end — request hops, JWT, sendDocument, persistence, response — with the data packet glowing as it moves.

%%{ init: { 'theme': 'dark', 'themeVariables': { 'primaryColor': '#1d4ed8', 'primaryTextColor': '#fff', 'primaryBorderColor': '#3b82f6', 'lineColor': '#60a5fa', 'secondaryColor': '#0b0d10', 'tertiaryColor': '#12151a', 'fontFamily': 'Inter, system-ui' }, 'flowchart': { 'curve': 'basis' } } }%%
flowchart LR
    classDef edge fill:#0b0d10,stroke:#3b82f6,color:#e5e7eb,stroke-width:1px;
    classDef api fill:#12151a,stroke:#3b82f6,color:#e5e7eb,stroke-width:1.5px;
    classDef store fill:#1e3a8a,stroke:#60a5fa,color:#fff,stroke-width:1.5px;
    classDef ext fill:#0b0d10,stroke:#26A5E4,color:#fff,stroke-width:2px;
    classDef pkt stroke:#22c55e,stroke-width:3px,fill:none,stroke-dasharray:8 4;

    User(["👤 You"]):::edge
    subgraph Browser["Next.js 14 · App Router"]
        MW["middleware.ts<br/>session gate"]:::api
        Dash["Dashboard<br/>TopBar · Dropzone · FileList · Stats"]:::api
    end
    subgraph Backend["FastAPI · Python 3.12"]
        Auth["routers/auth.py<br/>POST /auth/login"]:::api
        Files["routers/files.py<br/>upload · list · stream · patch · delete"]:::api
        TG["services/telegram.py<br/>3x exp-backoff retry"]:::api
    end
    DB[("PostgreSQL 16<br/>folders · files · soft-delete")]:::store
    TG_API(("Telegram Bot API<br/>private channel CDN")):::ext

    User -->|HTTPS| MW
    MW -->|unauth| Dash
    MW -->|authed| Dash
    Dash -->|POST /auth/login| Auth
    Auth -->|HS256 JWT| Dash
    Dash -->|"1. multipart upload, Bearer JWT"| Files
    Files -->|"2. size cap less than 2 GB"| Files
    Files -->|"3. sendDocument, caption=filename"| TG
    TG -->|"4. POST bot TOKEN sendDocument"| TG_API
    TG_API -->|"5. bytes stored in CDN"| TG_API
    TG -->|"6. file_id, message_id"| Files
    Files <-->|"7. INSERT files row"| DB
    Files -->|"8. 201 FileResponse"| Dash
    Dash -->|"9. invalidate files, stats"| User

    linkStyle 0,1,2,3,4 stroke:#60a5fa,stroke-width:1.5px;
    linkStyle 5,6,7,8,9 stroke:#22c55e,stroke-width:2.5px,stroke-dasharray:10 4;
Loading

🔵 = control plane (auth, navigation)  ·  🟢 = data plane (the actual bytes)


Component map

%%{ init: { 'theme': 'dark' } }%%
graph TB
    subgraph FE["frontend/"]
        direction LR
        L["app/(auth)/login"]:::fe
        P["app/page → Dashboard"]:::fe
        D["components/Dropzone"]:::fe
        L1["components/FileList"]:::fe
        S["components/StorageStats"]:::fe
        API["lib/api.ts (axios)"]:::fe
        AUTH["auth.ts (NextAuth v5)"]:::fe
    end
    subgraph BE["backend/app/"]
        direction LR
        RA["routers/auth.py"]:::be
        RF["routers/files.py"]:::be
        RFO["routers/folders.py"]:::be
        MW["middleware/auth.py<br/>require_auth"]:::be
        ST["services/telegram.py"]:::be
        DB["models/db.py<br/>Folder · File"]:::be
    end
    DB1[("PostgreSQL")]:::db
    TG(("Telegram CDN")):::ext

    L --> AUTH
    AUTH -->|"POST /auth/login"| RA
    P --> D
    P --> L1
    P --> S
    D --> API
    L1 --> API
    S --> API
    API -->|"Bearer JWT"| MW
    MW --> RA
    MW --> RF
    MW --> RFO
    RF --> ST
    RFO --> ST
    RF --> DB
    RFO --> DB
    ST --> TG
    DB --> DB1

    classDef fe fill:#0b0d10,stroke:#3b82f6,color:#e5e7eb;
    classDef be fill:#12151a,stroke:#22c55e,color:#e5e7eb;
    classDef db fill:#1e3a8a,stroke:#60a5fa,color:#fff;
    classDef ext fill:#0b0d10,stroke:#26A5E4,color:#fff,stroke-width:2px;
Loading

Data model

Two tables, no joins across hidden boundaries, soft-delete by convention. The tg_file_id column is sacred — it is the only durable handle back to your bytes.

%%{ init: { 'theme': 'dark' } }%%
erDiagram
    folders ||--o{ folders : "parent_id"
    folders ||--o{ files : "folder_id"
    folders {
        uuid id PK
        text name
        uuid parent_id FK
        text path "materialized"
        timestamp created_at
        timestamp updated_at
    }
    files {
        uuid id PK
        text name "mutable"
        text original_name
        text mime_type
        bigint size_bytes
        uuid folder_id FK
        text tg_file_id UK "⚠️ sacred"
        int  tg_message_id
        timestamp created_at
        timestamp updated_at
        timestamp deleted_at "soft-delete"
    }
Loading

🚀 User journey

The four interactions that make up 99% of what TGStore does — animated so you can feel the state changes.

① Sign in — first-time setup is a single login
%%{ init: { 'theme': 'dark', 'sequence': { 'actorMargin': 60, 'messageMargin': 40 } } }%%
sequenceDiagram
    autonumber
    actor U as 👤 You
    participant N as Next.js (middleware.ts)
    participant L as /login
    participant A as NextAuth v5
    participant B as FastAPI /auth/login
    participant DB as Postgres

    U->>N: GET /
    N-->>U: 307 → /login?next=/
    U->>L: open login form
    L->>A: signIn("credentials", user, pass)
    A->>B: POST /auth/login
    B->>B: bcrypt.compare(ADMIN_PASSWORD)
    B->>A: {access_token, expires_in}
    A->>A: encrypt → httpOnly cookie<br/>session.apiToken = JWT
    A-->>L: session
    L-->>U: redirect → /
    U->>N: GET / (with cookie)
    N-->>U: 200 Dashboard
    Note over B,DB: user row is the env file,<br/>Postgres holds files/folders
Loading
② Upload a file — drag, drop, done
%%{ init: { 'theme': 'dark' } }%%
flowchart LR
    classDef step fill:#0b0d10,stroke:#3b82f6,color:#e5e7eb,stroke-width:1.5px;
    classDef ok fill:#22c55e,stroke:#22c55e,color:#0b0d10,stroke-width:2px;
    classDef bad fill:#ef4444,stroke:#ef4444,color:#0b0d10,stroke-width:2px;

    S1["1. drop file on Dropzone"]:::step
    S2["2. client-side size check<br/>≤ 2 GB"]:::step
    S3["3. axios POST /files/upload<br/>multipart + Bearer JWT"]:::step
    S4["4. require_auth → 401?"]:::step
    S5["5. server size check (both<br/>Content-Length & body)"]:::step
    S6["6. telegram.send_document<br/>caption = filename"]:::step
    S7["7. extract file_id, message_id"]:::step
    S8["8. INSERT files row"]:::step
    S9["9. 201 FileResponse"]:::step
    S10["10. invalidate<br/>['files','stats']"]:::step
    R1["❌ 413 File too large"]:::bad
    R2["❌ 502 Storage unavailable"]:::bad

    S1 --> S2 --> S3 --> S4
    S4 -->|"no"| R1
    S4 -->|"yes"| S5 --> S6 --> S7 --> S8 --> S9 --> S10
    S6 -.->|"3× retry on 5xx"| R2
Loading
③ Download a file — proxy never leaks the bot token
%%{ init: { 'theme': 'dark' } }%%
sequenceDiagram
    autonumber
    actor U as 👤 You
    participant FE as FileRow (browser)
    participant BE as FastAPI /stream
    participant TG as Telegram getFile
    participant CDN as Telegram CDN

    U->>FE: click ⬇ Download
    FE->>BE: GET /files/{id}/stream<br/>Authorization: Bearer JWT
    BE->>BE: require_auth → claims
    BE->>TG: getFile(file_id)
    TG-->>BE: {file_path} (1h URL)
    BE->>CDN: GET file_path (follow_redirects)
    CDN-->>BE: 64 KB chunks
    BE-->>FE: StreamingResponse<br/>Content-Disposition: attachment
    FE->>FE: blob → URL.createObjectURL<br/>invisible <a download> click
    FE-->>U: 💾 file saved
    Note over BE,CDN: the bot token never leaves the backend
Loading
④ Browse & manage — search, rename, delete, stats
%%{ init: { 'theme': 'dark' } }%%
stateDiagram-v2
    [*] --> Idle
    Idle --> Searching: type in TopBar (300 ms debounce)
    Searching --> Idle: GET /files?search=…
    Idle --> Uploading: drop / pick / press U
    Uploading --> Progress: progress card
    Progress --> Idle: invalidate ['files','stats']
    Progress --> Error: network / 502
    Error --> Idle: retry toast
    Idle --> Renaming: click ✎ on row
    Renaming --> Idle: PATCH /files/{id}
    Idle --> Confirming: click 🗑 on row
    Confirming --> Idle: DELETE /files/{id} (soft)
    Idle --> Previewing: click 👁 (Phase 3)
    Previewing --> Idle: open in /stream
Loading

✨ Feature tour

🎯 Drag-and-drop upload
Multi-file, progress cards, 2 GB guard client-side.
🔐 JWT auth via NextAuth v5
Encrypted httpOnly cookies, edge middleware.
🗂 Folders (3 levels)
Materialized path, server-enforced depth.
🔍 Live search
300 ms debounce, case-insensitive, trigram-indexed.
📊 Storage stats
Stacked bar across Images / Videos / Audio / Docs / Other.
Proxied download
Bot token never reaches the browser.
🛡 Soft-delete
deleted_at only — Telegram message kept for recovery.
Streaming, not buffering
64 KB chunks, 5-min timeout, follow_redirects.
🧪 Tested
6 async integration tests, all Telegram calls mocked.

⚡ Quick start

Three terminals, one bot, zero vendor accounts. The diagram below shows what each command touches so you can keep mental model intact.

%%{ init: { 'theme': 'dark' } }%%
flowchart LR
    classDef t fill:#0b0d10,stroke:#3b82f6,color:#e5e7eb;
    classDef c fill:#12151a,stroke:#22c55e,color:#0b0d10,font-weight:bold;
    classDef a fill:#1e3a8a,stroke:#60a5fa,color:#fff;

    T1["terminal 1<br/>🗄 Postgres"]:::t
    T2["terminal 2<br/>🐍 FastAPI :8000"]:::t
    T3["terminal 3<br/>⚛ Next.js :3000"]:::t

    A1["docker compose up -d db"]:::c --> P1[("pgdata :5433")]:::a
    A2["uvicorn app.main:app --reload"]:::c --> P2[/"GET /health = 200"/]:::a
    A3["npm run dev"]:::c --> P3[/"http://localhost:3000"/]:::a
    P1 -.feeds.-> P2
    P2 -.http.-> P3
Loading

1. Provision Postgres

docker compose up -d db
# healthcheck gates the rest of the system

2. Backend

cd backend
python -m venv .venv && source .venv/bin/activate
pip install -e .
cp .env.example .env          # fill in BOT_TOKEN, CHAT_ID, JWT_SECRET
alembic upgrade head
uvicorn app.main:app --reload

Backend lives at http://localhost:8000 — OpenAPI docs at /docs.

3. Frontend

cd frontend
npm install
cp .env.example .env.local
npm run dev

Dashboard lives at http://localhost:3000.


🤖 One-time Telegram setup

%%{ init: { 'theme': 'dark' } }%%
sequenceDiagram
    autonumber
    actor U as 👤 You
    participant BF as @BotFather
    participant TG as Telegram
    participant JD as @JsonDumpBot
    participant ENV as backend/.env

    U->>BF: /newbot
    BF-->>U: BOT_TOKEN
    U->>TG: create private channel
    U->>TG: add bot as admin
    U->>TG: post any message
    U->>JD: forward that message
    JD-->>U: chat.id = -100xxxxxxxxxx
    U->>ENV: BOT_TOKEN=…<br/>CHAT_ID=-100…<br/>JWT_SECRET=$(openssl rand -base64 32)
    Note over U,ENV: that's the whole bootstrap
Loading

🔐 Environment variables

Backend — backend/.env

Var Required Default Purpose
BOT_TOKEN From @BotFather. Never sent to the browser.
CHAT_ID Private channel id, negative, e.g. -100xxxxxxxxxx.
DATABASE_URL postgresql+asyncpg://tgstore:tgstore@localhost:5433/tgstore Async SQLAlchemy URL.
DATABASE_URL_SYNC postgresql+psycopg2://… Used by Alembic.
JWT_SECRET openssl rand -base64 32.
JWT_EXPIRE_HOURS 24 Token lifetime.
ADMIN_USERNAME admin Single-user login.
ADMIN_PASSWORD changeme Set this in prod.
ALLOWED_ORIGINS http://localhost:3000 Comma-separated CORS allowlist.
MAX_UPLOAD_BYTES 2147483648 2 GB — Telegram's Bot API cap.
ENVIRONMENT development Set to production on Railway/Vercel. Triggers fail-closed checks on dev defaults and mock auth (see Secret rotation).

Frontend — frontend/.env.local

Var Required Purpose
NEXT_PUBLIC_API_URL http://localhost:8000 in dev, your Railway URL in prod.
AUTH_SECRET openssl rand -base64 32 — NextAuth cookie encryption.
AUTH_URL http://localhost:3000 in dev.
NEXT_PUBLIC_FIREBASE_* ✅ (for Firebase auth) Web SDK config from Firebase Console → Project Settings → Your apps. Not strictly secret (Firebase web API keys are public-by-design); add App Check + HTTP referrer restrictions in GCP.

🔁 Secret rotation

If any of these secrets have lived on a developer disk in plaintext, on a shared machine, in a backup, or in version control at any point — assume they are public and rotate them. The backend Settings class refuses to boot in production with dev defaults (see ENVIRONMENT above), so a fresh deploy cannot accidentally run with admin_password=changeme or jwt_secret=dev-secret-change-me.

Order matters — do these in one sitting, in this order. A half-rotated state is more dangerous than not rotating at all (an old, still-trusted secret + a new, partly-deployed one = easy to forget which is which).

1. Generate new values (offline)

Secret How to generate
JWT_SECRET openssl rand -hex 32
AUTH_SECRET openssl rand -hex 32
ADMIN_PASSWORD openssl rand -base64 24 (or your own 20+ char password)
BOT_TOKEN Telegram → @BotFather → /revoke (picks the bot) → /token (gets the new one)
DATABASE_URL / DATABASE_URL_SYNC Railway → Postgres service → "Data" → "Reset Password" → copy the new connection string into both URLs
Firebase SA key GCP Console → tgstore-a9d23 → IAM & Admin → Service Accounts → firebase-adminsdk-fbsvc@… → Keys → Add KeyCreate new key (JSON)

Store the new values in a password manager note called "TGStore new secrets" before touching any production system. Do not write them into backend/.env yet.

2. Local env (do this first)

rm backend/firebase-sa.json backend/.env   # remove the old plaintext copies
# Recreate backend/.env with the NEW values from step 1.
# Use backend/.env.example as a template.
# Prefer FIREBASE_SERVICE_ACCOUNT_JSON over FIREBASE_SERVICE_ACCOUNT_PATH
# so no service-account file lives on disk.
# Add ENVIRONMENT=development (so the safety checks are off locally).

Verify locally boots: cd backend && uvicorn app.main:app and hit http://localhost:8000/health.

3. Production env vars (Railway + Vercel, in one sitting)

Railway → TGStore service → Variables — set:

BOT_TOKEN=<new>                          # Phase 1.5
DATABASE_URL=<new with rotated password> # Phase 1.4
DATABASE_URL_SYNC=<new with rotated password>
JWT_SECRET=<new>                         # Phase 1.1
ADMIN_PASSWORD=<new>                     # Phase 1.3
ALLOWED_ORIGINS=https://tgstore.vercel.app
FIREBASE_SERVICE_ACCOUNT_JSON=<new SA JSON, one line>
FIREBASE_MOCK_AUTH=false
ENVIRONMENT=production                   # enables fail-closed checks

Vercel → TGStore project → Settings → Environment Variables — set:

AUTH_SECRET=<new>                        # Phase 1.2
AUTH_URL=https://tgstore.vercel.app
NEXT_PUBLIC_API_URL=<unchanged>

Both deploys auto-redeploy on save. Watch Railway's deploy log for INFO: Firebase initialized successfully using service account credentials JSON string.

4. Invalidate the old keys

After the new values are live:

  • Firebase: GCP Console → Service Accounts → Keys → delete the old key (the one with the private_key_id from before the rotation). Verify only one key remains. The old private key is now useless.
  • Telegram: the old token is already revoked by the /revoke step in Phase 1. Sanity-check: curl -i https://api.telegram.org/bot<old>/getMe returns 404.
  • Postgres: the old password is already replaced by the "Reset Password" step. Nothing to delete on the DB side.
  • JWT_SECRET / AUTH_SECRET / ADMIN_PASSWORD: there's no external system to revoke; the old values are simply no longer trusted by the backends. All in-flight sessions are invalidated (every user re-logs in; this is expected).

5. Verify

# Backend health (unauthenticated):
curl -fsS https://<railway>.up.railway.app/health

# Login with the NEW password:
curl -fsS -X POST https://<railway>.up.railway.app/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"<new>"}'

# Login FAILS with the OLD password (must return 401):
curl -i -X POST https://<railway>.up.railway.app/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"<old>"}'

Then in a real browser: sign in on https://tgstore.vercel.app, upload a file, download it, sign out and back in. The full happy-path must work end to end.

6. Why this is hard to mess up

The fail-closed checks added in backend/app/core/config.py (see the production_safety_checks validator) mean that if you forget to set any one of the production secrets — JWT_SECRET, ADMIN_PASSWORD, or a Firebase service account — the backend refuses to start when ENVIRONMENT=production. The deploy will roll back to the last healthy revision and the Railway log will tell you exactly which env var is missing. This is intentional: a deploy that boots with admin_password=changeme is a five-minute account takeover; a deploy that fails to boot is just a 30-second retry.


📡 API reference

All routes below (except /auth/login and /health) require Authorization: Bearer <jwt>. Full OpenAPI lives at /docs when the backend is running.

%%{ init: { 'theme': 'dark' } }%%
flowchart LR
    classDef pub fill:#0b0d10,stroke:#3b82f6,color:#e5e7eb;
    classDef priv fill:#12151a,stroke:#22c55e,color:#e5e7eb;
    classDef unsafe fill:#3f1d1d,stroke:#ef4444,color:#fecaca;

    H["GET /health"]:::pub
    L["POST /auth/login"]:::pub
    M["GET /auth/me"]:::priv
    U["POST /files/upload"]:::priv
    LI["GET /files"]:::priv
    ST["GET /files/stats"]:::priv
    G["GET /files/{id}"]:::priv
    S["GET /files/{id}/stream"]:::priv
    D["GET /files/{id}/download-url"]:::unsafe
    P["PATCH /files/{id}"]:::priv
    DE["DELETE /files/{id}"]:::priv
    FC["POST /folders"]:::priv
    FL["GET /folders"]:::priv
    FP["PATCH /folders/{id}"]:::priv
    FD["DELETE /folders/{id}"]:::priv
Loading
Method Path Auth Notes
GET /health Liveness probe.
POST /auth/login {username, password}{access_token, expires_in}.
GET /auth/me 🔒 Current user info.
POST /files/upload 🔒 Multipart, folder_id?, 2 GB cap.
GET /files 🔒 page, limit≤100, search, folder_id, mime_type, include_deleted.
GET /files/stats 🔒 StorageStats grouped by MIME.
GET /files/{id} 🔒 Single file metadata.
GET /files/{id}/stream 🔒 Use this for browser downloads — proxied bytes.
GET /files/{id}/download-url 🔒 ⚠️ Backend-only. Embeds the bot token — never expose to the client.
PATCH /files/{id} 🔒 Rename and/or move. tg_file_id is preserved.
DELETE /files/{id} 🔒 Soft-delete.
POST /folders 🔒 Body {name, parent_id?}. 3-level depth cap.
GET /folders 🔒 ?parent_id=… to list children.
PATCH /folders/{id} 🔒 Rename.
DELETE /folders/{id} 🔒 Refuses if non-empty.

🚢 Deploy

The minimal, no-surprises path:

Service Why
Vercel (frontend) Edge middleware runs on the edge, NextAuth cookies just work.
Railway (backend) One Dockerfile, healthcheck on /health, persistent env.
Neon (Postgres) Free tier, branching for previews.
Telegram (storage) The only "CDN" you need.

Pre-flight checklist (from Docs/Ai Instruction.md):

  • BOT_TOKEN and CHAT_ID are in Railway env, not in code.
  • DATABASE_URL points to Neon, not localhost.
  • ALLOWED_ORIGINS includes the Vercel frontend URL.
  • JWT_SECRET is openssl rand -base64 32 — not secret / dev.
  • NEXT_PUBLIC_API_URL in Vercel points at the Railway backend URL.
  • alembic upgrade head runs on every backend deploy.

🗺 Roadmap

%%{ init: { 'theme': 'dark' } }%%
gantt
    title TGStore milestones
    dateFormat  YYYY-MM-DD
    axisFormat  %b
    section Phase 1 (shipped)
    Scaffold + Docker compose     :done, p1a, 2025-11-01, 7d
    FastAPI foundation + auth    :done, p1b, after p1a, 7d
    Telegram service + retry     :done, p1c, after p1b, 5d
    Files router (CRUD + stream) :done, p1d, after p1c, 7d
    Next.js dashboard + upload   :done, p1e, after p1d, 7d
    E2E verification             :done, p1f, after p1e, 3d
    section Phase 2 (next)
    Folder polish + breadcrumbs  :active, p2a, 2026-06-15, 7d
    Move-to-folder UX            :         p2b, after p2a, 5d
    Search v2 (filters, trigram) :         p2c, after p2b, 5d
    Hard-delete + Telegram purge :         p2d, after p2c, 3d
    section Phase 3
    Inline image preview         :         p3a, after p2d, 5d
    Video / audio / PDF preview  :         p3b, after p3a, 7d
    Share-link (signed URL)      :         p3c, after p3b, 5d
Loading

📐 Non-negotiables

These rules are baked into the repo. If you fork it, keep them.

%%{ init: { 'theme': 'dark' } }%%
mindmap
  root((TGStore<br/>rules))
    Security
      Bot token never in browser
      JWT in httpOnly cookie only
      Telegram download URLs never cached
      4xx errors never retried
    Storage
      Single object per file (no chunking)
      tg_file_id is sacred and unique
      Soft-delete only at this phase
      2 GB cap enforced pre-Telegram
    Code
      async def everywhere
      Pydantic v2 only
      Alembic for all schema changes
      Server Components by default
    What we don't do
      No S3 / local disk
      No multi-user
      No payment logic
      No requests library
Loading

🗂 Repo layout

TGStore/
├── backend/                    # FastAPI · Python 3.12+
│   ├── app/
│   │   ├── main.py             # app factory, CORS, router mount
│   │   ├── core/               # config (pydantic-settings) + async DB
│   │   ├── middleware/         # JWT: create_access_token, require_auth
│   │   ├── routers/            # auth · files · folders
│   │   ├── models/             # SQLAlchemy ORM + Pydantic schemas
│   │   ├── services/           # telegram.py (the only place that hits api.telegram.org)
│   │   └── utils/              # helpers, MIME grouping
│   ├── alembic/                # 0001_initial.py
│   ├── tests/                  # 6 async integration tests, Telegram mocked
│   ├── pyproject.toml
│   └── .env.example
├── frontend/                   # Next.js 14 · App Router · TypeScript strict
│   ├── app/
│   │   ├── layout.tsx
│   │   ├── page.tsx            # → Dashboard
│   │   ├── providers.tsx       # QueryClient + SessionProvider
│   │   ├── globals.css
│   │   ├── (auth)/login/       # /login
│   │   └── api/auth/[…]        # NextAuth route handlers
│   ├── components/             # Dashboard · TopBar · Dropzone · FileList · FileRow · StorageStats · ApiAuthBridge
│   ├── lib/                    # api.ts (axios) · format.ts
│   ├── types/                  # mirrors backend Pydantic
│   ├── auth.ts                 # NextAuth v5 config
│   ├── middleware.ts           # edge session gate
│   └── tailwind.config.ts
├── Docs/
│   ├── PRD.md                  # product spec
│   ├── TRD.md                  # technical spec
│   ├── APP FLOW.md             # user journey
│   └── Ai Instruction.md       # the rules
├── docker-compose.yml          # Postgres 16 on host 5433
└── README.md                   # you are here

🧪 Testing

# backend (Telegram calls mocked, in-memory SQLite for speed)
cd backend
pytest -v

# 6 tests, ~1.5s:
#   ✓ test_health_is_unauthenticated
#   ✓ test_login_success_returns_jwt
#   ✓ test_login_failure_returns_401
#   ✓ test_protected_endpoint_requires_auth
#   ✓ test_upload_2gb_cap_is_enforced_before_telegram
#   ✓ test_upload_happy_path_persists_metadata

📚 Further reading

  • Docs/PRD.md — product spec, scope, success criteria
  • Docs/TRD.md — technical spec, data model, endpoint contracts
  • [Docs/APP FLOW.md](Docs/APP FLOW.md) — every user journey, every error state
  • [Docs/Ai Instruction.md](Docs/Ai Instruction.md) — the non-negotiables


footer

Built with care · Backed by Telegram · Owned by you.


About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages