FTStore now holds both halves of the product:
ios/ the swift app — the fork of claration/Feather, unchanged apart
from its new home
web/ FTStore Admin, formerly its own repository. serves the
dashboard at /admin, the ftstore.json feed and IPA downloads
app-repo.json and Images/ stay at the repo root on purpose: the former is
a published AltStore source whose raw.githubusercontent URL is what
installed copies resolve updates through, so moving it would break every
subscriber.
The admin server is renamed throughout: package ftstore-admin, images
ghcr.io/<owner>/ftstore-admin-{app,worker}, postgres role/db ftstore,
session cookie ftstore_session, temp dir /tmp/ftstore, backup type
ftstore-settings, outbound user-agent FTStoreAdmin. backups written under the old
name still import, and compose is pinned to name: ftstore so the
volume prefix does not follow the directory name.
apply-rebrand.sh gains a relocation step: upstream's tree is rooted at
the repo root while ours lives in ios/, so a merge drops Feather's files
next to ios/ and the script folds them in before renaming. it never
touches web/. still idempotent.
release tags are namespaced — v* for the app, web-v* for the server —
so a server release no longer triggers a macOS build or rewrites
app-repo.json. workflows are prefixed ios-/web- and path-filtered.
9.6 KiB
Contributing to FTStore Admin
This covers web/ only — the Next.js dashboard and feed server. The iOS app
lives in ios/ and has its own guide in
CONTRIBUTING.md at the repo root. Every command below is
run from web/.
Prerequisites
- Node.js 20+
- PostgreSQL 16+ (or Docker)
- npm 10+
Optional (for full pipeline):
- Telegram API credentials (my.telegram.org)
- GitHub personal access token with
reposcope
Running as a User (Docker)
The fastest way to get everything running.
1. Clone and configure
git clone https://github.com/FTStuff/FTStore.git
cd FTStore/web
cp .env.example .env
Edit .env with your credentials:
TELEGRAM_API_ID=12345
TELEGRAM_API_HASH=abc123...
TELEGRAM_PHONE=+1234567890
TELEGRAM_CHANNELS=channel1,channel2
STORAGE_DIR=/app/storage
2. Start all services
docker compose up -d
This starts three containers:
| Service | Port | Description |
|---|---|---|
db |
5432 | PostgreSQL database |
app |
3000 | Next.js dashboard |
worker |
— | Telegram scanner (background) |
The app container runs database migrations automatically on startup.
3. Open the dashboard
Visit http://localhost:3000. You'll land on the Dashboard page.
4. Seed default settings (first run)
docker compose exec app npx prisma db seed
5. Volume Permissions
Each container runs as a specific non-root user. If you encounter permission errors, fix ownership on the Docker volume data:
| Volume | Owner (UID:GID) | Error symptom |
|---|---|---|
tdlib-data |
1001:1001 | EACCES: permission denied, mkdir '/app/tdlib-data/db' |
ipa-storage |
1001:1001 | Storage directory /app/storage is not writable at startup |
temp-downloads |
1001:1001 | EACCES: permission denied on /tmp/ftstore |
pgdata |
70:70 | FATAL: could not open file "global/pg_filenode.map": Permission denied |
For named volumes:
# Fix ownership
sudo chown -R 1001:1001 /var/lib/docker/volumes/ftstore_tdlib-data/_data
sudo chown -R 1001:1001 /var/lib/docker/volumes/ftstore_ipa-storage/_data
sudo chown -R 1001:1001 /var/lib/docker/volumes/ftstore_temp-downloads/_data
sudo chown -R 70:70 /var/lib/docker/volumes/ftstore_pgdata/_data
If you're using bind mounts (e.g. at /opt/docker/ftstore):
sudo chown -R 1001:1001 /opt/docker/ftstore/tdlib-data
sudo chown -R 1001:1001 /opt/docker/ftstore/storage
sudo chown -R 1001:1001 /opt/docker/ftstore/temp-downloads
sudo chown -R 70:70 /opt/docker/ftstore/pgdata
ipa-storage must be the same volume (or host path) in the app and worker
services: the worker writes what the scraper downloads, the app serves it and
writes dashboard uploads.
Alternatively, you can remove the volumes and let them be recreated with correct permissions (⚠️ deletes database and session data):
docker compose down
docker volume rm ftstore_tdlib-data ftstore_ipa-storage ftstore_temp-downloads ftstore_pgdata
docker compose up -d
6. Updating / Redeploying
The Telegram session is preserved across redeployments. Both services use stop_grace_period: 30s so TDLib can close its database cleanly, and the worker retries connecting with backoff after a restart.
docker compose pull # pull latest images
docker compose up -d # recreate containers (session preserved)
Important: Do not use
docker compose down -vunless you want to delete the Telegram session and database. Use plaindocker compose downto keep volume data.
7. Stopping
docker compose down # stop containers (keeps data)
docker compose down -v # stop and delete all data
Running as a Developer
1. Clone and install
git clone https://github.com/FTStuff/FTStore.git
cd FTStore/web
npm install
2. Set up the database
Start a local PostgreSQL instance. With Docker:
docker run -d --name ftstore-db \
-e POSTGRES_USER=ftstore \
-e POSTGRES_PASSWORD=ftstore \
-e POSTGRES_DB=ftstore \
-p 5432:5432 \
postgres:16-alpine
3. Configure environment
cp .env.example .env
Set at minimum:
DATABASE_URL="postgresql://ftstore:ftstore@localhost:5432/ftstore?schema=public"
Add Telegram/GitHub credentials if you want to test the full pipeline.
4. Initialize the database
npx prisma generate # generate the Prisma client
npx prisma migrate dev # create tables and apply migrations
npm run db:seed # populate default settings
If you're iterating on the schema without needing migrations:
npx prisma db push # sync schema to DB (no migration files)
5. Start the dev server
npm run dev
Open http://localhost:3000.
6. Start the worker (optional)
The worker requires valid Telegram credentials and TDLib. In a separate terminal:
npm run worker
This connects to Telegram, scans configured channels, and processes the download queue.
Project Structure
src/
├── middleware.ts # Gates /admin, the feed and downloads; everything
│ # else is answered as a dead host
├── app/
│ ├── admin/ # The dashboard — ftstore.app/admin
│ │ ├── (auth)/ # login, register
│ │ └── (dashboard)/ # dashboard, metrics, library, queue, logs, settings
│ ├── api/ # REST endpoints (session-gated, except auth/health)
│ ├── ftstore.json/ # The AltStore feed the iOS app reads
│ ├── d/ # /d/<token>/<name>.ipa — public IPA downloads
│ └── media/icons/ # Tweak icons referenced by the feed
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── layout/ # Sidebar, header, page container
│ └── ... # Feature-specific components
├── lib/
│ ├── ipa/ # IPA extraction and analysis
│ ├── telegram/ # TDLib client, scanner, downloader
│ ├── github/ # Inbound scanning of third-party release sources
│ ├── storage/ # Local IPA/icon store, retention, quota, HTTP serving
│ ├── appstore/ # iTunes lookup with caching
│ ├── json/ # Feed generation and serving
│ ├── pipeline/ # Queue, orchestrator, ingest
│ └── cleanup/ # Log and cache cleanup
├── hooks/ # React hooks (polling, etc.)
└── types/ # Shared TypeScript types
worker/
└── index.ts # Standalone Telegram scanner process
Common Tasks
Adding a shadcn/ui component
npx shadcn@latest add <component-name>
Updating the database schema
- Edit
prisma/schema.prisma - Run
npx prisma migrate dev --name describe-your-change - The Prisma client regenerates automatically
Running a production build locally
npm run build
npm start
Regenerating JSON feeds manually
Hit the API endpoint:
curl -X POST http://localhost:3000/api/actions/generate-json
Or use the Settings page > Admin Actions > Generate JSON.
Triggering a channel scan
curl -X POST http://localhost:3000/api/actions/scan
Checking system health
curl http://localhost:3000/api/health
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | — | PostgreSQL connection string |
TELEGRAM_API_ID |
For worker | — | Telegram API ID |
TELEGRAM_API_HASH |
For worker | — | Telegram API hash |
TELEGRAM_PHONE |
For worker | — | Phone number for Telegram auth |
TELEGRAM_CHANNELS |
For worker | — | Comma-separated channel usernames |
GITHUB_TOKEN |
No | — | Fallback PAT for the FTStore Manager proxy only |
APPSTORE_COUNTRY |
No | us |
App Store region for lookups |
SCAN_INTERVAL_MINUTES |
No | 30 |
How often the worker scans channels |
JSON_REGEN_INTERVAL_MINUTES |
No | 60 |
How often JSON feeds regenerate |
CLEANUP_INTERVAL_HOURS |
No | 24 |
How often old versions and orphaned files are cleaned |
MAX_VERSIONS_PER_APP |
No | 5 |
Versions kept per app in JSON feeds |
TEMP_DIR |
No | /tmp/ftstore |
Temporary download directory |
STORAGE_DIR |
No | /app/storage |
Where IPAs and tweak icons are stored |
MAX_STORAGE_GB |
No | 0 |
Storage cap in GiB; the scraper stops there (0 = unlimited) |
LOG_RETENTION_DAYS |
No | 30 |
Days before old logs are deleted |
Architecture Notes
- The worker runs as a separate process because TDLib maintains a persistent stateful connection to Telegram that doesn't fit the Next.js request lifecycle.
- The worker and web app communicate exclusively through the shared PostgreSQL database — there is no direct IPC or message queue.
- IPA files, the JSON feed and tweak icons are all served by this server. The feed is rendered from the database per request (
src/lib/json/serve-feed.ts) rather than published anywhere; downloads go through/d/<token>/<name>.ipa, guarded by a random capability token and, optionally, the FTStore User-Agent. - GitHub is only used for inbound scraping of third-party release repositories (
src/lib/github/, each source carrying its own token) and for the FTStore Manager proxy. - The storage cap is enforced as queue policy, not in the ingest step: the worker stops claiming work while the cap is reached, so manual dashboard uploads still succeed.