# ARCHITECTURE — TikTok Manager

This document describes the system architecture, design patterns, module responsibilities, and data flow.

---

## 1. System Overview

```
┌─────────────────────────────────────────────────────┐
│           tiktok-manager container (web + API)       │
│                                                      │
│  ┌──────────────────────────────────────────────┐   │
│  │              entrypoint.sh                    │   │
│  │  1. Wait for the DB (python -m app.db_wait)  │   │
│  │  2. flask db upgrade (Alembic migrations)    │   │
│  │  3. gunicorn (2 workers, optional TLS)       │   │
│  └──────────────────────────────────────────────┘   │
│                                                      │
│  ┌──────────────────────────────────────────────┐   │  ┌────────────────────────────┐
│  │              Flask Application                │   │  │  tiktok-scheduler container │
│  │                                               │   │  │  python -m app.scheduler    │
│  │  ┌─────────┐ ┌──────────┐ ┌─────────┐         │   │  │  (plain loop, no gunicorn,  │
│  │  │ auth_bp │ │account_bp│ │ post_bp │         │   │  │   no ports — ADR-005)       │
│  │  └─────────┘ └──────────┘ └─────────┘         │   │  └────────────┬───────────────┘
│  │  ┌───────────────┐ ┌─────────┐                │   │               │
│  │  │ insights_bp   │ │  api_bp │                │   │               │
│  │  └───────────────┘ └─────────┘                │   │               │
│  └──────────────────────────────────────────────┘   │               │
│                                                      │               │
│  ┌──────────────┐ ┌──────────┐ ┌───────────────┐   │               │
│  │ tiktok_api   │ │ models.py│ │   auth.py     │   │               │
│  │   .py        │ │(Account, │ │ (Login +      │   │               │
│  │ (TikTok API  │ │Scheduled │ │ token refresh │   │               │
│  │  facade)     │ │  Post)   │ │ + guard)      │   │               │
│  └──────────────┘ └──────────┘ └───────────────┘   │               │
│                                                      │               │
│  ┌──────────────────────────────────────────────┐   │               │
│  │  SQLAlchemy — engine chosen by DB_ENGINE:    │   │               │
│  │   sqlite   → /app/data/tiktok_manager.db     │   │               │
│  │   postgres → host "db", port 5432 (below)    │   │               │
│  └──────────────────────────────────────────────┘   │               │
└─────────────────────────────────────────────────────┘               │
          │ compose network (profile "postgres", ADR-004)              │
          ▼                                                             ▼
┌─────────────────────────────────────┐        ┌────────────────────────────────┐
│  db: postgres:17 container          │        │  shared volumes: ./data (DB),  │
│  named volume pgdata, healthcheck   │        │  ./media (scheduled videos)    │
└─────────────────────────────────────┘        └────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────┐
│  TikTok APIs (open.tiktokapis.com): │
│  Login Kit · Display API ·          │
│  Content Posting API                │
└─────────────────────────────────────┘
```

---

## 2. Module Responsibilities

### `app/__init__.py` — Application Factory
- Creates Flask app via `create_app()`; loads config, runs the fail-closed startup guards,
  initializes SQLAlchemy and Flask-Migrate (batch mode for SQLite), registers the blueprints,
  exempts the API blueprint from CSRF (it is header-authenticated)
- Never creates tables: the schema comes from `flask db upgrade` in `entrypoint.sh` (ADR-004)

### `app/config.py` — Configuration
- Reads all environment variables with defaults; `check_secrets_config()` refuses placeholder
  `SECRET_KEY` / `API_KEY` / `TIKTOK_CLIENT_SECRET` and a missing `TOKEN_ENCRYPTION_KEY` in
  production; `database_uri()` builds the engine URL from `DB_ENGINE` (ADR-004)

### `app/db_wait.py` — Database readiness (entrypoint helper)
- `wait_for_database(uri, timeout)`: retries a connection until it succeeds or
  `DB_CONNECT_TIMEOUT_SECONDS` elapse; SQLite returns at once

### `migrations/` — Alembic (Flask-Migrate)
- One revision per schema change, named after the change ID (`0001_initial.py`:
  `accounts` + `scheduled_posts`); applied on every start by `entrypoint.sh`

### `app/models.py` — Data Model
- `Account`: a TikTok account (unique `open_id`); access + refresh tokens Fernet-encrypted
  (write-only properties, read through `access_token_plain()` / `refresh_token_plain()`);
  `expires_at` stored as **naive UTC** (engine-neutral); profile and stats columns
- `ScheduledPost`: a video waiting for its scheduled time (ADR-005) — caption, privacy, mode,
  `video_path` (file under `MEDIA_FOLDER`) xor `video_url`, `scheduled_at` (naive UTC), status
  machine `pending → publishing → published | failed`, `publish_id` / `video_id` / safe `error`
- `to_dict()` on both: serializes for API responses — never the tokens

### `app/auth.py` — Authentication Layer
- TikTok Login flow: `get_auth_url()` (state generated per flow, stored in the session),
  `save_account_from_login()` (code → tokens → user info → upsert Account)
- `ensure_fresh_token(account)`: refreshes the 24-hour access token when expired or within
  10 minutes of expiry, persists TikTok's **rotated** refresh token, returns the access token —
  used by every adapter and by the scheduler before each publish
- `@login_required`: session-only guard, **no first-account fallback**

### `app/tiktok_api.py` — TikTok API Facade
- Plain `requests` calls against `open.tiktokapis.com` (Login Kit token endpoint, Display API
  user/video endpoints, Content Posting API init/status) and the chunked `PUT` upload
- Flask-free and DB-free; every failure raises `TikTokAPIError` with a **client-safe message**
  (fixed strings per status/error code, e.g. token invalid, scope missing, unaudited-app
  privacy rule, rate limit); the raw payload is logged server-side only
- `publish_video_file()` orchestrates init + 32 MB chunked upload; `wait_for_publish()` polls
  the async processing status up to ~90 s

### `app/services/` — Use cases shared by GUI and API (ADR-002, from day one)
- `errors.py`: typed errors (`ValidationError`, `NotFound`, `Forbidden`, `UpstreamError`)
- `uploads.py`: `store_upload()` — server-generated uuid names, extension allow-list
  (mp4/mov/webm), realpath confinement, optional size cap
- `posts.py`: `validate_post_fields()` (caption ≤ 2,200 chars, privacy allow-list, direct/draft
  mode, video xor URL) and `publish_now()` (temp file for uploads, always cleaned up in
  `finally`; waits for the processing verdict)
- `scheduled.py`: the ADR-005 use cases — `create_scheduled_post` (file stored under
  `MEDIA_FOLDER`), `reschedule_post`, `retry_post`, `delete_scheduled_post` (row + file), and
  `run_due_posts()` — the worker function the scheduler container calls

### `app/scheduler.py` — Scheduler entry point (ADR-005)
- `python -m app.scheduler`: a plain loop — every `SCHEDULER_INTERVAL_SECONDS`, run
  `run_due_posts()` inside an app context; per-tick exceptions are logged and the loop
  continues. Runs in its own container (`tiktok-scheduler`), never inside a gunicorn worker

### `app/routes/auth_routes.py` — Login Endpoints
- `/`, `/login`, `/auth/start`, `/auth/callback`, `POST /logout`

### `app/routes/account_routes.py` — Dashboard & Accounts
- `/dashboard`, `/accounts`, `/accounts/add`, `POST /accounts/{id}/activate`,
  `POST /accounts/{id}/refresh`, `POST /accounts/{id}/delete` — every state change is POST

### `app/routes/post_routes.py` — Posts (Web UI)
- `/posts` (published videos), `GET+POST /posts/new` (publish now or schedule),
  `/posts/scheduled`, `POST /scheduled/{id}/reschedule`, `POST /scheduled/{id}/retry`,
  `POST /scheduled/{id}/delete`

### `app/routes/insights_routes.py` — Insights (Web UI)
- `/insights` — account totals and the top-videos-by-views ranking

### `app/routes/api_routes.py` — REST API (17 endpoints)
- All under `/api/v1`; `@api_key_required` (constant-time compare);
  `resolve_account(require_explicit)` — mutations need an explicit account, reads may fall
  back to the first connected one; `tiktok_error_response()` maps `TikTokAPIError` to envelope
  codes with safe messages

---

## 3. Data Flow

### 3.1 TikTok Login Flow

```
User → /login → /auth/start → TikTok Login dialog
                                        │
                                        ▼
                          /auth/callback?code=...&state=...
                                        │
                                        ▼
                    save_account_from_login()
                          ├─ state verified against the session
                          ├─ exchange_code() → access (24h) + refresh token, open_id, scopes
                          ├─ get_user_info() → display name, avatar, stats
                          └─ upsert Account (tokens Fernet-encrypted, expires_at naive UTC)
                                        │
                                        ▼
                          session["active_account_id"] = account.id
                                        │
                                        ▼
                                    /dashboard
```

### 3.2 Token refresh (every adapter + the scheduler)

```
ensure_fresh_token(account)
    │
    ├─ expires_at more than 10 min away? → return access_token_plain()
    │
    └─ else: refresh_token() at TikTok
         ├─ persist NEW access token
         ├─ persist the ROTATED refresh token (old one is invalidated by TikTok)
         ├─ expires_at = now + expires_in (naive UTC)
         └─ commit → return the fresh access token
```

### 3.3 Publish now (shared by GUI and API — ADR-002)

```
GUI: POST /posts/new (form)            API: POST /api/v1/posts (JSON, video_url)
 or POST /api/v1/posts/media (multipart file)        │
    │                                                │
    └────────────────┬───────────────────────────────┘
                     ▼
        ensure_fresh_token(account) → token
                     ▼
        services.posts.validate_post_fields()
          (caption ≤ 2,200, privacy allow-list, mode, video xor URL)
                     ▼
        file? → store_upload() → /app/uploads/<uuid>.mp4 (temp)
                     ▼
        publish_video_file()  — init + 32 MB chunked PUT
        or init_video_pull()  — PULL_FROM_URL
                     ▼
        wait_for_publish() — poll status ≤ ~90 s
                     ▼
        finally: remove_quietly() (temp file always deleted)
                     ▼
        {publish_id, publish_status, video_id} → flash (GUI) / envelope 201 (API)
```

### 3.4 Scheduled publish (ADR-005)

```
create: GUI /posts/new (scheduled_at)  ·  API POST /api/v1/scheduled[/media]
    → validate + parse_scheduled_time (future, aware → stored naive UTC)
    → file? store under MEDIA_FOLDER (persistent)
    → ScheduledPost(status=pending)

tiktok-scheduler container (every SCHEDULER_INTERVAL_SECONDS):
    run_due_posts()
        ├─ SELECT pending WHERE scheduled_at <= now (naive UTC)
        ├─ claim: pending → publishing (commit)
        ├─ ensure_fresh_token(account)
        ├─ publish_video_file / init_video_pull → wait_for_publish
        ├─ published → publish_id/video_id recorded, stored file deleted
        └─ failed   → safe error message recorded (retry / reschedule later)
```

---

## 4. Design Pattern Catalog

| Pattern | Where | Why |
|---------|-------|-----|
| **Factory** | `create_app()` | Configurable app creation, test isolation |
| **Blueprint** | All route modules | Modular routing, parallel development |
| **Decorator** | `@login_required`, `@api_key_required` | Auth guards without code duplication |
| **Facade** | `tiktok_api.py` | Simplify the three TikTok APIs behind small functions |
| **Adapter** | `TikTokAPIError` → envelope codes / flash categories | One error shape at the edge, safe messages everywhere |
| **DTO** | API response envelope, `to_dict()` | Consistent data transfer |
| **Active Record** | `Account`, `ScheduledPost` models | Simple DB access for a two-model app |
| **Template Method** | `base.html` | Shared page structure |
| **Service Layer** | `services/posts.py`, `services/scheduled.py` (ADR-002) | One validation for GUI and API — parity is structural |
| **Worker loop** | `app/scheduler.py` (ADR-005) | One process, one writer, idempotent ticks — no scheduler library |

Named deliberately; ordinary code carries no pattern labels (see `CODING-CONVENTIONS.md` §10).

---

## 5. Technology Stack and External Dependencies

| Layer | Technology | Purpose | Version |
|-------|-----------|---------|---------|
| Language runtime | Python | container base image `python:3.12-slim` | 3.12 |
| Web framework | Flask | app factory, blueprints, routing | 3.1.3 |
| ORM | Flask-SQLAlchemy (SQLAlchemy) | `Account` / `ScheduledPost` persistence on both engines | 3.1.1 (SQLAlchemy 2.1.4) |
| Schema migrations | Flask-Migrate / Alembic | one revision per change, `flask db upgrade` on every start (ADR-004) | 4.1.0 / 1.20.0 |
| CSRF | Flask-WTF / WTForms | token in every state-changing form; API blueprint exempt | 1.3.0 / 3.2.2 |
| Token encryption | cryptography | Fernet / MultiFernet for access + refresh tokens at rest | 50.0.1 |
| TikTok API client | requests | every call in `tiktok_api.py` | 2.33.0 |
| Env loading | python-dotenv | `.env` → `config.py` | 1.2.2 |
| WSGI server | gunicorn | 2 workers, 300s timeout; `--certfile/--keyfile` when `SSL_*` are set | 22.0.0 |
| Database (default) | SQLite | single file under `DATABASE_PATH` (ADR-004) | — |
| Database (large) | PostgreSQL container | `postgres:17.6-alpine`, profile `postgres` (ADR-004) | 17+ |
| PostgreSQL driver | psycopg[binary] | v3, postgres mode only | 3.3.6 |
| Containerization | Docker / Docker Compose | app + scheduler + optional `db` | — |
| Front end | Bootstrap / Bootstrap Icons | dark-theme UI with TikTok pink/cyan accents (CDN) | 5.3.3 / 1.11.3 |
| Local HTTPS (dev) | mkcert | locally-trusted certificate for the HTTPS OAuth redirect URI TikTok requires | 1.4.4 |
| Testing | pytest | unit + route tests, fakes for every TikTok call | 9.1.1 |
| Dependency audit | pip-audit | blocks a change with known vulnerabilities | 2.10.1 |

`pip-audit` is clean as of the initial build (see the initial change fragment); versions above
were verified against the virtualenv (`pip list`), the Dockerfile base image and the CDN links
in the templates — not quoted from memory.

---

## 6. Security Posture (honest, as of 2026-10-13)

The rules are in `SECURITY.md`. Like the sibling Facebook Manager, this app was **born under
the rules** (constant-time API key compare, CSRF everywhere, POST-only state changes,
explicit-account mutations, session-only login, Fernet-encrypted tokens, server-named uploads,
safe error messages, startup guards) — plus TikTok-specific care: rotated refresh tokens are
persisted on every refresh, and the scheduler records only client-safe error messages. What is
still open is listed honestly in `SECURITY.md` §4 (TT-S-01..08).

---

## 7. Layering

Built as ADR-002 prescribes from the first line (no migration debt):

```
routes/*_routes.py (GUI adapters)   routes/api_routes.py (API adapters)
               └──────────────┬──────────────┘
                              ▼
                      services/<domain>.py        validation, use case, typed errors
                              │
                              ▼
                    tiktok_api.py                 facade over the TikTok APIs (no DB, no Flask)
                              │
                              ▼
                    models.py / auth.py           persistence, OAuth, token refresh
```

The scheduler container calls `services/scheduled.py` directly — the same use case the adapters
call, never its own copy of the logic.

---

## 8. Known Drift and Debt

Tracked in `FEATURE-REGISTRY.md` (status `drift` / `gap`) and `SECURITY.md` §4. As of the
initial build there is **no parity drift**: every GUI capability has an API twin or a reasoned
browser exception. One recorded product gap: TikTok's Comment API (needs an audited app) — see
the registry.
