Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ReadZero app build-time configuration.
#
# Copy to .env and fill in the values, then build with:
# flutter run --dart-define-from-file=.env
#
# Everything in this file is compiled into the app binary, so ONLY put values
# here that are safe to ship to users. The Supabase anon key is public by
# design (row level security protects the data).
#
# Edge Function secrets (OpenRouter, Jina, xAI, model ids) do NOT belong here.
# They live server-side; see supabase/functions/.env.example and set them with
# `supabase secrets set`.

SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_ANON_KEY=<supabase-anon-key>
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: pub
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
riverpod:
patterns:
- "flutter_riverpod"
- "riverpod_*"

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
flutter:
name: Flutter analyze and test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
# Keep in sync with ios/ci_scripts/ci_post_clone.sh
flutter-version: 3.27.3
channel: stable
cache: true

- run: flutter --version

- run: flutter pub get

# Pre-existing warnings are tracked as a follow-up; drop
# --no-fatal-warnings once they are cleared.
- run: flutter analyze --no-fatal-infos --no-fatal-warnings

- run: flutter test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
pubspec.lock

# Symbolication related
app.*.symbols
Expand Down Expand Up @@ -103,6 +102,7 @@ supabase/.temp/
.env
.env.*
*.env
!.env.example

# Firebase
**/ios/Runner/GoogleService-Info.plist
Expand Down
214 changes: 111 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,147 +1,155 @@
# ReadZero - Intelligent Reading List
# ReadZero

A cross-platform mobile app that serves as a smart reading list destination. Share any article, blog post, or web page, and get AI-powered daily digests with intelligent summaries.
Read-it-later for iOS with AI daily digests and podcast playback. Share a link from any app, ReadZero extracts the article, analyzes it with Claude, and each morning delivers a digest of everything you saved. Digests can also be listened to as podcast episodes.

![Flutter](https://img.shields.io/badge/Flutter-3.19+-blue)
![Supabase](https://img.shields.io/badge/Supabase-Backend-green)
![Claude AI](https://img.shields.io/badge/Claude-Haiku%203.5-orange)
[![App Store](https://img.shields.io/badge/App_Store-ReadZero-black?logo=apple)](https://apps.apple.com/app/readzero/id6758777327)
![Flutter](https://img.shields.io/badge/Flutter-3.27-blue)
![Supabase](https://img.shields.io/badge/Supabase-Postgres_+_Edge_Functions-green)
![Claude](https://img.shields.io/badge/Claude-Haiku_4.5_via_OpenRouter-orange)
[![CI](https://github.com/bluzername/ReadZero/actions/workflows/ci.yml/badge.svg)](https://github.com/bluzername/ReadZero/actions/workflows/ci.yml)

- App Store: <https://apps.apple.com/app/readzero/id6758777327>
- Website, support, privacy and terms: <https://bluzername.github.io/ReadZero/>

## Features

- **📱 Share Extension** - Share from any app on iOS
- **📄 Full Content Extraction** - Extracts article text, images, and comments
- **🐦 X/Twitter Support** - Native X post extraction via Grok API
- **🤖 AI Analysis** - Multi-level summaries using Claude via OpenRouter
- **📊 Daily Digest** - Intelligent summaries with theme detection
- **🌙 Dark Mode Icons** - iOS 18+ dark mode app icon support
- **🎨 Beautiful Reader UI** - Clean, modern interface
- **Save from anywhere**: iOS Share Extension writes the URL to an App Group; the app syncs it to Supabase and kicks off extraction.
- **Full content extraction**: Mozilla Readability on the Edge Function, Jina Reader as fallback, Unsplash cover image fallback with photographer credit.
- **X and LinkedIn**: X posts are fetched through Grok's `x_search` tool (xAI). LinkedIn and X can also be extracted on-device through an authenticated `WKWebView` session (`features/auth`).
- **AI analysis**: TL;DR, key points, deeper bullets, topics, sentiment, reading time and content type per article, generated by Claude through OpenRouter.
- **Daily digest**: a pg_cron job calls `generate-digest` every morning; the digest has an overall summary, top themes, per-article highlights and cross-article insights. A local notification fires when it lands.
- **Podcast playback**: digests can be turned into audio episodes, played in-app (mini player, `just_audio`) or subscribed to as a personal RSS feed from any podcast app.
- **Library and archive**: reader UI with markdown rendering, archive, delete, mark as read, JSON export of your data.
- **Anonymous accounts**: the app signs in anonymously on first launch; no sign-up required.

## How it works

```
iOS Share Extension --> App Group --> Flutter app --> Supabase (Postgres, Auth, Realtime)
|
trigger: articles insert -> processing_queue
|
process-queue (Edge Function)
|
extract-article (Edge Function)
Readability / Jina / Grok + Claude via OpenRouter
|
pg_cron 08:00 UTC -> generate-digest (Edge Function) -> digests
```

## Architecture
### Repository layout

```
┌─────────────────────────────────────────────────────────────┐
│ Flutter App │
│ Share Extension → App Group → Main App → Supabase Sync │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Supabase Backend │
│ PostgreSQL │ Edge Functions │ Realtime │ Auth │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ External Services │
│ Jina Reader │ Claude (OpenRouter) │ Grok API (xAI) │
└─────────────────────────────────────────────────────────────┘
lib/
main.dart App entry: Supabase init, anonymous sign-in, share channel
core/
config/env.dart Build-time config (dart-define)
models/models.dart Article, DailyDigest, PodcastEpisode, UserSettings, ...
router/app_router.dart go_router: /, /digest, /settings, /article/:id, /archive
services/ Supabase access, local notifications, authenticated extraction
theme/app_theme.dart Design system
features/
articles/ Article detail, discover cards, providers
auth/ Web login for LinkedIn/X cookie-based extraction
digest/ Daily digest screen
home/ Library
podcast/ Player state and mini player
settings/ Settings and archive
supabase/
migrations/ Schema, processing queue, cron job
functions/
_shared/config.ts Model ids (CLAUDE_MODEL, GROK_MODEL)
extract-article/ Extraction + Claude analysis
generate-digest/ Daily digest generation
process-queue/ Drains processing_queue, calls extract-article
ios/
Runner/, ShareExtension/ App and share extension
ci_scripts/ci_post_clone.sh Xcode Cloud bootstrap
scripts/validate_and_upload.sh Local archive + App Store Connect upload
website/ GitHub Pages site (index, support, privacy, terms)
docs/ Markdown sources for the legal and support pages
test/ Unit tests (models, restricted domains, config)
```

## Setup

### Prerequisites

- Flutter SDK 3.19+
- Supabase account
- OpenRouter API key (for Claude)
- Jina AI API key (for article extraction)
- xAI API key (for X/Twitter extraction)
- Flutter 3.27.3 (the version pinned in CI and Xcode Cloud)
- Xcode with an iOS simulator or device
- A Supabase project
- API keys: OpenRouter (Claude), Jina (fallback extraction), xAI (X posts). Optional: Tavily, Unsplash.

### 1. Clone & Install Dependencies
### 1. Clone and install

```bash
git clone https://github.com/bluzername/ReadZero.git
cd ReadZero
flutter pub get
```

### 2. Supabase Setup
### 2. App configuration (dart-define)

1. Create a new Supabase project at [supabase.com](https://supabase.com)
The app never reads config from disk. `SUPABASE_URL` and `SUPABASE_ANON_KEY` are compiled in with `--dart-define`.

2. Run the database migrations:
```bash
# Copy the SQL from supabase/migrations/
# Paste and run in Supabase SQL Editor
```
```bash
cp .env.example .env # fill in your project URL and anon key
flutter run --dart-define-from-file=.env
```

3. Deploy Edge Functions:
```bash
# Install Supabase CLI
npm install -g supabase
Debug builds assert with a clear message when the defines are missing; release builds log and run in offline mode. Only put values in `.env` that are safe to ship inside the app binary (the anon key is public by design; row level security protects the data).

# Login and link
supabase login
supabase link --project-ref YOUR_PROJECT_REF
### 3. Supabase backend

# Set secrets
supabase secrets set OPENROUTER_API_KEY=your_key
supabase secrets set JINA_API_KEY=your_key
supabase secrets set XAI_API_KEY=your_key
```bash
npm install -g supabase
supabase login
supabase link --project-ref YOUR_PROJECT_REF

# Deploy functions
supabase functions deploy extract-article --no-verify-jwt
supabase functions deploy generate-digest --no-verify-jwt
```
# Schema (articles, digests, user_settings, processing_queue, triggers)
supabase db push
```

### 3. Configure the App
`supabase/migrations/20260205_setup_daily_digest_cron.sql` contains `<SUPABASE_URL>` and `<SUPABASE_ANON_KEY>` placeholders. Substitute them before running it (or store the key in Supabase Vault as described in the file comment) so the key is never committed.

Edit `lib/core/config/env.dart`:
### 4. Edge Function secrets and model ids

```dart
class Env {
static const String supabaseUrl = 'https://YOUR_PROJECT.supabase.co';
static const String supabaseAnonKey = 'YOUR_ANON_KEY';
}
```bash
cp supabase/functions/.env.example supabase/functions/.env # fill in
supabase secrets set --env-file supabase/functions/.env

supabase functions deploy extract-article --no-verify-jwt
supabase functions deploy generate-digest --no-verify-jwt
supabase functions deploy process-queue --no-verify-jwt
```

### 4. iOS Setup
| Variable | Used by | Default |
| --- | --- | --- |
| `OPENROUTER_API_KEY` | extract-article, generate-digest | required |
| `JINA_API_KEY` | extract-article | required |
| `XAI_API_KEY` | extract-article | required |
| `TAVILY_API_KEY` | extract-article | optional |
| `UNSPLASH_ACCESS_KEY` | extract-article | optional |
| `CLAUDE_MODEL` | extract-article, generate-digest | `anthropic/claude-haiku-4.5` (OpenRouter slug) |
| `GROK_MODEL` | extract-article | `grok-4-1-fast` (xAI model id) |

The iOS project is pre-configured with:
- Share Extension for saving URLs
- App Groups for extension communication
- Dark mode app icons
`SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are injected by the Supabase runtime; do not set them as secrets.

Bundle ID: `live.bluzername.readzero.app`
### 5. iOS

### 5. Run the App
Bundle ID `live.bluzername.readzero.app`. The Xcode project ships with the Share Extension, App Group and dark-mode icons configured.

```bash
# iOS
flutter run -d ios --release
```
- **Xcode Cloud**: set `SUPABASE_URL` and `SUPABASE_ANON_KEY` as environment variables on the workflow. `ios/ci_scripts/ci_post_clone.sh` installs Flutter, then runs `flutter build ios --config-only` with those defines so `Generated.xcconfig` carries them into the archive.
- **Local release upload**: `ASC_APP_PASSWORD=... ./ios/scripts/validate_and_upload.sh` reads `.env` the same way, then archives and uploads to App Store Connect.

## Project Structure
## Development

```bash
flutter analyze --no-fatal-infos --no-fatal-warnings
flutter test
```
lib/
├── main.dart # App entry point
├── core/
│ ├── config/env.dart # Environment configuration
│ ├── models/models.dart # Data models
│ ├── router/app_router.dart # Navigation
│ ├── services/supabase_service.dart
│ └── theme/app_theme.dart # Design system
├── features/
│ ├── articles/ # Article display
│ ├── digest/ # Daily digest
│ ├── home/ # Library view
│ └── settings/ # Settings

supabase/
├── migrations/ # Database schema
└── functions/
├── extract-article/ # Content extraction + Grok
└── generate-digest/ # AI digest generation

ios/
├── Runner/ # Main app
└── ShareExtension/ # iOS share extension

docs/
├── privacy-policy.md # Privacy Policy
└── terms-of-service.md # Terms of Service
```
CI (`.github/workflows/ci.yml`) runs the same on every push and pull request. Dependabot keeps pub packages and GitHub Actions current.

## License

Expand Down
Loading
Loading