Files
readest/docker
Huang Xin d1e7b4902c feat(share): time-limited share links with cfi-aware imports (#4037)
Add a Share Book feature that generates an expiring HTTPS share URL plus a
parallel readest://share/{token} deep link. Recipients land on /s/{token},
where logged-in users can one-tap "Add to my library" (R2 server-side
byte-copy) and anonymous users download the book or open it in the app.
Sharers manage active links from a dedicated "Manage Shared Links" panel
under user settings.

Highlights:
- 9 new App Router endpoints under /api/share (create, [token], cover,
  og.png, download, download/confirm, import, revoke, list).
- /s landing page with branded next/og chat unfurl image and SSR auth-cookie
  detection so logged-in recipients see "Add to my library" as the primary
  action without layout shift.
- Server-side R2 byte-copy for /import preserves the project's existing
  invariant that every files.file_key is prefixed with its row's user_id;
  stats / purge / delete / download routes work unchanged. URL-encodes the
  copy source so titles with spaces or '&' don't break the copy.
- Universal 7-day expiry cap, no tier differentiation, no "never". DMCA-risk
  reduction. Picker defaults to 3 days.
- Position-aware shares: "Share current page" toggle (off by default for
  privacy) attaches the sharer's CFI; recipient lands at the same paragraph.
- Per-user 50-share cap, rate limiting via Cache-Control: no-store on
  token-bearing responses, atomic SQL increment for download_count via a
  SECURITY DEFINER function so the public confirm beacon stays safe under
  concurrent fire.
- Soft revocation: presigned download URLs (5-min TTL) cannot be cancelled
  before TTL; documented as accepted v1 behavior.
- token + token_hash hybrid storage: public endpoints look up by hash and
  never select the raw token, so accidental SELECT-* leakage on a public
  route can't expose the bearer credential.
- Mobile / desktop Tauri share via tauri-plugin-sharekit; web falls back to
  navigator.share with a clipboard fallback when no native share method
  exists. Share-sheet dismissal no longer silently copies.

UI:
- New Dialog with a settings-card group: iOS-style segmented duration picker
  + toggle slider for "Share current page", on a single row each.
- Reader top-bar Share button, library context-menu Share entry, manage-
  shares list with cover thumbnails and overflow menu.
- New <SegmentedControl> primitive in src/components for reuse.

Coverage:
- Unit tests for token utils + URL parser (20 new tests, full suite at 3445).
- 31 locales translated for all new strings; en plurals hand-added per the
  project's hand-curated en convention.

DB migration in docker/volumes/db/migrations/002_add_book_shares.sql adds
the book_shares table, RLS policies, and the increment_book_share_download
RPC. Migration is idempotent.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:03:35 +02:00
..

Self-Hosting with Docker/Podman with Compose

Stack

service Image Description
client from ../Dockerfile readest frontend
db supabase/postgres psql db with supabase extensions
kong kong:2.8.1 api gateway routing requests to supabase services
auth supabase/gotrue:v2.185.0 auth service (email, JWT)
rest postgrest/postgrest:v14.3 psql rest api
minio minio/minio s3 storage
minio-setup minio/mc helper container to create s3 buckets

Exposed ports

Port Service
3000 readest
7000 kong API gateway
9000 MinIO S3 API
9001 MinIO console UI

Running with Docker/Podman Compose

1. setup .env

cp docker/.env.example docker/.env

update docker/.env:

  • update POSTGRES_PASSWORD to a strong password (32+ chars)
  • update JWT_SECRET to a random secret (32+ chars)
  • regenerate ANON_KEY and SERVICE_ROLE_KEY as HS256 JWTs signed with your JWT_SECRET (use jwt.io or a similar tool):
    • ANON_KEY payload: {"role": "anon"}
    • SERVICE_ROLE_KEY payload: {"role": "service_role"}
  • set MINIO_ROOT_PASSWORD to a strong password

2. Start the Stack

run from the docker/ directory:

cd docker
docker compose up --build -d

the client image is built locally on first run. subsequent starts reuse the cached image.

3. Access

  • Readest app: http://localhost:3000
  • MinIO console: http://localhost:9001 (login with MINIO_ROOT_USER / MINIO_ROOT_PASSWORD)

Hot Reload (development)

to develop using the compose stack, set the build target on client to development-stage, which'll runs the next.js dev server. to enable hot reload, uncomment the volumes block in the client service in compose.yaml:

volumes:
  - ../:/app
  - /app/node_modules
  - /app/apps/readest-app/node_modules
  - /app/apps/readest-app/public/vendor
  - /app/apps/readest-app/.next
  - /app/packages/foliate-js/node_modules

the first mount overlays your local repo into the container. the remaining anonymous volumes shadow the directories that were pre-built inside the image, so the container's installed deps and vendor assets are used instead of what's on your host.

Stop the Stack

cd docker
docker compose down

to also remove volumes (database and storage data):

cd docker
docker compose down -v

Building the Dockerfile standalone

the Dockerfile requires Build args for the next.js public env vars (they are inlined at build time)

docker build \
  --target production-stage \
  --build-arg NEXT_PUBLIC_SUPABASE_URL=http://localhost:7000 \
  --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key> \
  --build-arg NEXT_PUBLIC_APP_PLATFORM=web \
  --build-arg NEXT_PUBLIC_API_BASE_URL=http://localhost:3000 \
  --build-arg NEXT_PUBLIC_OBJECT_STORAGE_TYPE=s3 \
  --build-arg NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824 \
  --build-arg NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA=50000 \
  -t readest-client \
  .

run the built image:

docker run -p 3000:3000 \
  -e SUPABASE_URL=http://kong:8000 \
  -e SUPABASE_ANON_KEY=<anon-key> \
  -e SUPABASE_ADMIN_KEY=<service-role-key> \
  -e S3_ENDPOINT=http://localhost:9000 \
  -e S3_REGION=us-east-1 \
  -e S3_BUCKET_NAME=readest-files \
  -e S3_ACCESS_KEY_ID=<minio-user> \
  -e S3_SECRET_ACCESS_KEY=<minio-password> \
  readest-client