Skip to content
Merged
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
17 changes: 9 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
# (copy to .env and fill in; .env is gitignored). Doppler is the source of truth.
#
# --- Storage ---------------------------------------------------------------
# Option A: managed cloud database (Turso) — has managed backups.
TRONBROWSER_DB_URL=libsql://your-db.turso.io
TRONBROWSER_DB_AUTH_TOKEN=your-turso-token
# Postgres. The API refuses to start on anything else (no file fallback).
# TRONBROWSER_DB_URL is accepted as an alias for the same value.
# The password belongs in Doppler, not here; this placeholder has none.
DATABASE_URL=postgres://tronbrowser@localhost:5432/tronbrowser
#
# Option B: bring your own SQLite — you own your data (and your backups).
# Local file:
# Legacy Turso/libSQL database, kept only until the Postgres cutover is proven.
# Read by scripts/db-migrate.mjs for the old schema and by the one-off copy:
# npx libsql-pg copy --from "$TRONBROWSER_DB_URL" --token "$TRONBROWSER_DB_AUTH_TOKEN" --to "$DATABASE_URL" --verify
# TRONBROWSER_DB_URL=libsql://your-db.turso.io
# TRONBROWSER_DB_AUTH_TOKEN=your-turso-token
# TRONBROWSER_DB_PATH=/home/you/.tronbrowser/db.sqlite
# Or your own libSQL server:
# TRONBROWSER_DB_URL=libsql://db.your-server.example
# TRONBROWSER_DB_AUTH_TOKEN=your-token
#
# --- AI providers (Bring Your Own Keys) -----------------------------------
# Free / self-hosted: bring your own keys. The paid cloud tier uses OUR keys,
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "tronbrowser",
"version": "3.8.8",
"private": true,
"description": "TronBrowser.dev — open-source, privacy-first, AI-native browser",
"description": "TronBrowser.dev \u2014 open-source, privacy-first, AI-native browser",
"packageManager": "pnpm@9.12.0",
"engines": {
"node": ">=24"
Expand All @@ -22,11 +22,12 @@
"@types/node": "^24.0.0",
"typescript": "^5.6.3",
"vitest": "^2.1.4",
"@libsql/client": "^0.14.0",
"@eslint/js": "^9.18.0",
"eslint": "^9.18.0",
"globals": "^15.14.0",
"typescript-eslint": "^8.62.0"
"typescript-eslint": "^8.62.0",
"@profullstack/libsql-pg": "^0.1.1",
"@libsql/client": "^0.14.0"
},
"main": "index.js",
"directories": {
Expand Down
24 changes: 14 additions & 10 deletions packages/storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,23 @@ backups/replication; everything else is self-hosted and user-managed.

## Migrations

Schema lives in [`migrations/`](migrations/) as ordered `NNNN_name.sql` files,
applied by a forward-only runner that tracks applied files in a
The API runs on Postgres (via `@profullstack/libsql-pg`, which keeps the
`@libsql/client` surface the code was written against). Schema lives in
[`migrations-pg/`](migrations-pg/) as ordered `NNNN_name.sql` files, generated
from the SQLite originals in [`migrations/`](migrations/) with
`npx libsql-pg convert-schema` and reviewed. `scripts/db-migrate.mjs` applies
whichever directory matches the URL it is given and records each file in a
`schema_migrations` table (idempotent).

```bash
doppler run -- pnpm db:migrate # apply pending migrations to the configured DB
pnpm db:status # show applied vs pending (no changes)
DATABASE_URL=postgres://... pnpm db:migrate # apply pending Postgres migrations
pnpm db:status # show applied vs pending
```

The runner ([`scripts/db-migrate.mjs`](../../scripts/db-migrate.mjs)) reads the
same `TRONBROWSER_DB_URL`/`_AUTH_TOKEN`/`_PATH` env, so it targets Turso, your
own libSQL server, or a local SQLite file. Current migrations:
`0001_ai_provider_keys`, `0002_accounts_settings` (anonymous CoinPay + email/
password accounts, sessions, synced settings).
The SQLite files stay until the Turso cutover is proven; a `libsql://` or
`file:` URL still applies them. Moving the rows:

See [`.env.example`](../../.env.example) and the [PRD](../../docs/tronbrowser-prd.md).
```bash
npx libsql-pg copy --from "$TRONBROWSER_DB_URL" --token "$TRONBROWSER_DB_AUTH_TOKEN" \
--to "$DATABASE_URL" --verify
```
21 changes: 21 additions & 0 deletions packages/storage/migrations-pg/0001_ai_provider_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- TronBrowser Postgres schema: 0001_ai_provider_keys
-- Generated from packages/storage/migrations/0001_ai_provider_keys.sql with `npx libsql-pg convert-schema`, then reviewed.
-- Forward-only; scripts/db-migrate.mjs applies this directory when TRONBROWSER_DB_URL is postgres://.

-- created_at: text column with a current-time default became timestamptz
-- updated_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
-- updated_at: default (datetime('now')) -> now()
create table if not exists ai_provider_keys (
app_id text NOT NULL,
provider text NOT NULL,
api_key text NOT NULL,
enabled bigint NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (app_id, provider)
);

CREATE INDEX IF NOT EXISTS idx_ai_provider_keys_app
ON ai_provider_keys (app_id)
WHERE enabled = 1;
51 changes: 51 additions & 0 deletions packages/storage/migrations-pg/0002_accounts_settings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- TronBrowser Postgres schema: 0002_accounts_settings
-- Generated from packages/storage/migrations/0002_accounts_settings.sql with `npx libsql-pg convert-schema`, then reviewed.
-- Forward-only; scripts/db-migrate.mjs applies this directory when TRONBROWSER_DB_URL is postgres://.

-- created_at: text column with a current-time default became timestamptz
-- updated_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
-- updated_at: default (datetime('now')) -> now()
create table if not exists users (
id text PRIMARY KEY,
auth_method text NOT NULL DEFAULT 'coinpay',
coinpay_sub text UNIQUE,
email text UNIQUE,
email_verified bigint NOT NULL DEFAULT 0,
password_hash text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);

-- updated_at: text column with a current-time default became timestamptz
-- updated_at: default (datetime('now')) -> now()
create table if not exists user_settings (
user_id text PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
settings text NOT NULL DEFAULT '{}',
updated_at timestamptz NOT NULL DEFAULT now()
);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists email_tokens (
token text PRIMARY KEY,
user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
purpose text NOT NULL,
-- written as now() + interval and compared with now(): timestamptz, not text
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists sessions (
token text PRIMARY KEY,
user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- written as now() + interval and compared with now(): timestamptz, not text
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users (email) WHERE email IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions (user_id);
103 changes: 103 additions & 0 deletions packages/storage/migrations-pg/0003_extension_store.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
-- TronBrowser Postgres schema: 0003_extension_store
-- Generated from packages/storage/migrations/0003_extension_store.sql with `npx libsql-pg convert-schema`, then reviewed.
-- Forward-only; scripts/db-migrate.mjs applies this directory when TRONBROWSER_DB_URL is postgres://.

-- created_at: text column with a current-time default became timestamptz
-- updated_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
-- updated_at: default (datetime('now')) -> now()
create table if not exists extensions (
id text PRIMARY KEY,
slug text NOT NULL UNIQUE,
owner_user_id text NOT NULL,
name text NOT NULL,
summary text,
description text,
homepage_url text,
icon_url text,
status text NOT NULL DEFAULT 'draft',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (owner_user_id) REFERENCES users(id)
);

CREATE INDEX IF NOT EXISTS idx_extensions_owner ON extensions (owner_user_id);

CREATE INDEX IF NOT EXISTS idx_extensions_status ON extensions (status, updated_at);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists extension_versions (
id text PRIMARY KEY,
extension_id text NOT NULL,
version text NOT NULL,
manifest_version bigint NOT NULL,
manifest_json text NOT NULL,
permissions_json text,
bundle_url text,
crx_url text,
bundle_sha256 text,
size_bytes bigint,
source text NOT NULL DEFAULT 'upload',
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (extension_id) REFERENCES extensions(id),
UNIQUE (extension_id, version)
);

CREATE INDEX IF NOT EXISTS idx_ext_versions_ext ON extension_versions (extension_id, created_at DESC);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists extension_payments (
id text PRIMARY KEY,
extension_id text NOT NULL,
user_id text NOT NULL,
amount_cents bigint NOT NULL DEFAULT 100,
currency text NOT NULL DEFAULT 'usd',
method text NOT NULL,
status text NOT NULL DEFAULT 'pending',
provider_ref text,
paid_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (extension_id) REFERENCES extensions(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX IF NOT EXISTS idx_ext_payments_ext ON extension_payments (extension_id);

CREATE INDEX IF NOT EXISTS idx_ext_payments_ref ON extension_payments (provider_ref);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists extension_scans (
id text PRIMARY KEY,
extension_id text NOT NULL,
version_id text NOT NULL,
provider text NOT NULL DEFAULT 'vu1nz',
status text NOT NULL DEFAULT 'pending',
score bigint,
severity text,
findings_json text,
error text,
scanned_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (extension_id) REFERENCES extensions(id),
FOREIGN KEY (version_id) REFERENCES extension_versions(id)
);

CREATE INDEX IF NOT EXISTS idx_ext_scans_ext ON extension_scans (extension_id, created_at DESC);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists extension_flags (
id text PRIMARY KEY,
extension_id text NOT NULL,
reporter_user_id text,
reason text NOT NULL,
detail text,
status text NOT NULL DEFAULT 'open',
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (extension_id) REFERENCES extensions(id)
);

CREATE INDEX IF NOT EXISTS idx_ext_flags_ext ON extension_flags (extension_id, status);
17 changes: 17 additions & 0 deletions packages/storage/migrations-pg/0004_extension_publishers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- TronBrowser Postgres schema: 0004_extension_publishers
-- Generated from packages/storage/migrations/0004_extension_publishers.sql with `npx libsql-pg convert-schema`, then reviewed.
-- Forward-only; scripts/db-migrate.mjs applies this directory when TRONBROWSER_DB_URL is postgres://.

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists publisher_keys (
user_id text PRIMARY KEY,
handle text NOT NULL UNIQUE,
pubkey text NOT NULL,
fingerprint text NOT NULL,
provisioned_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX IF NOT EXISTS idx_publisher_keys_handle ON publisher_keys (handle);
17 changes: 17 additions & 0 deletions packages/storage/migrations-pg/0005_publisher_tokens.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- TronBrowser Postgres schema: 0005_publisher_tokens
-- Generated from packages/storage/migrations/0005_publisher_tokens.sql with `npx libsql-pg convert-schema`, then reviewed.
-- Forward-only; scripts/db-migrate.mjs applies this directory when TRONBROWSER_DB_URL is postgres://.

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists publisher_tokens (
id text PRIMARY KEY,
user_id text NOT NULL,
token_hash text NOT NULL UNIQUE,
name text,
created_at timestamptz NOT NULL DEFAULT now(),
last_used_at timestamptz,
FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX IF NOT EXISTS idx_publisher_tokens_user ON publisher_tokens (user_id);
17 changes: 17 additions & 0 deletions packages/storage/migrations-pg/0006_extension_signing_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- TronBrowser Postgres schema: 0006_extension_signing_keys
-- Generated from packages/storage/migrations/0006_extension_signing_keys.sql with `npx libsql-pg convert-schema`, then reviewed.
-- Forward-only; scripts/db-migrate.mjs applies this directory when TRONBROWSER_DB_URL is postgres://.

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists extension_signing_keys (
extension_id text PRIMARY KEY,
crx_id text NOT NULL,
public_key_der text NOT NULL,
private_key_enc text NOT NULL,
key_algo text NOT NULL DEFAULT 'rsa-2048',
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (extension_id) REFERENCES extensions(id)
);

CREATE INDEX IF NOT EXISTS idx_signing_keys_crx_id ON extension_signing_keys(crx_id);
43 changes: 43 additions & 0 deletions packages/storage/migrations-pg/0007_push.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- TronBrowser Postgres schema: 0007_push
-- Generated from packages/storage/migrations/0007_push.sql with `npx libsql-pg convert-schema`, then reviewed.
-- Forward-only; scripts/db-migrate.mjs applies this directory when TRONBROWSER_DB_URL is postgres://.

-- created_at: text column with a current-time default became timestamptz
-- last_seen_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
-- last_seen_at: default (datetime('now')) -> now()
create table if not exists push_devices (
id text PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
last_seen_at timestamptz NOT NULL DEFAULT now()
);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists push_subscriptions (
token text PRIMARY KEY,
device_id text NOT NULL,
origin text NOT NULL,
app_key text,
created_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
FOREIGN KEY (device_id) REFERENCES push_devices(id)
);

CREATE INDEX IF NOT EXISTS idx_push_subscriptions_device ON push_subscriptions (device_id);

-- created_at: text column with a current-time default became timestamptz
-- created_at: default (datetime('now')) -> now()
create table if not exists push_messages (
id text PRIMARY KEY,
token text NOT NULL,
device_id text NOT NULL,
body text NOT NULL,
encoding text,
urgency text NOT NULL DEFAULT 'normal',
topic text,
expires_at bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_push_messages_device ON push_messages (device_id, expires_at);
Loading
Loading