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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Production uses privacy-preserving first-party analytics through Supabase for bo

Tally supports two deliberately different sharing choices:

- **Start live activity** creates a short capability URL for one canonical activity in Supabase. Trusted recipients with the complete link can load and edit the same revision-checked data from different browsers. The Live invite can be copied, opened from a QR code, or sent through the device share sheet.
- **Start live activity** creates a short capability URL for one canonical activity in Supabase. Trusted recipients with the complete link can load and edit the same revision-checked data from different browsers. The Live invite can be copied, opened from a QR code, or sent through the device share sheet. Any holder of the complete link can explicitly end that capability; previously opened browsers keep their last recovery copy.
- **Share balances only** exports a PNG summary with totals and suggested payments without granting access to the activity.
- If Safari opens a Live link outside the installed PWA, **Join activity** safely transfers the copied link into the existing Tally app session.

Expand All @@ -41,14 +41,15 @@ For launch copy, a privacy-safe demo storyboard, and channel guidance, see the [
- Persist data in the browser and synchronize changes across open tabs.
- Install Tally as a PWA and reopen the local app shell without a network connection.
- Collaborate across browsers through short, revision-checked live activity links that automatically load newer changes while visible.
- End a Live link immediately without deleting the last synced recovery copies on participating devices.
- Continue a Safari-opened shared link in an installed Tally PWA without abandoning the existing app session.
- Measure anonymous local and live feature usage without sending activity data or secret URLs to analytics.
- Use the responsive interface on desktop or mobile.
- Switch between English and Simplified Chinese, with China-aware defaults and local-time expense timestamps.

## Important data note

Local activities remain in browser `localStorage` and can be viewed after the installed app shell has been cached for offline use. Live activities are stored in Supabase and are editable by anyone with the full capability link while connected. Each participating browser also keeps the latest full Live snapshot locally: if the connection is unavailable, that recovery copy is read-only so offline changes cannot silently conflict. A person can explicitly duplicate it into an independent editable activity, or continue it locally after the backend confirms that the Live session has ended. There are no user accounts or participant-level permissions. Read [PRIVACY.md](PRIVACY.md) before deploying or sharing real activity data.
Local activities remain in browser `localStorage` and can be viewed after the installed app shell has been cached for offline use. Live activities are stored in Supabase and are editable—and can be ended—by anyone with the full capability link while connected. Each participating browser also keeps the latest full Live snapshot locally: if the connection is unavailable, that recovery copy is read-only so offline changes cannot silently conflict. A person can explicitly duplicate it into an independent editable activity, or continue it locally after the backend confirms that the Live session has ended. There are no user accounts or participant-level permissions. Read [PRIVACY.md](PRIVACY.md) before deploying or sharing real activity data.

Currency selection controls display only. Tally does not convert amounts or support mixed currencies inside one activity.

Expand Down Expand Up @@ -151,7 +152,7 @@ Every push and pull request must pass:
- ESLint with TypeScript and React Hooks rules and zero warnings;
- component and helper tests;
- Playwright integration tests against the production GitHub Pages build;
- pgTAP contracts for live-activity and analytics access control, validation, privacy, retention, and rate limits;
- pgTAP contracts for live-activity and analytics access control, graph validation, revocation, privacy, retention, and per-client/project-wide rate limits;
- 100% statement, branch, function, and line coverage;
- a production static build.

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Include the affected flow, reproduction steps, impact, and any suggested mitigat

Local activities and identities are stored in browser `localStorage`. Live activities are stored in a private Supabase schema and accessed only through capability-checked public RPC wrappers. The edit token remains in the URL fragment and only its SHA-256 hash is stored by the backend.

Anyone with a complete live URL can read and edit that activity. There are no accounts, participant-level permissions, token revocation, or audit trail in this release. Treat leaked URLs as compromised and avoid regulated or highly sensitive data.
Anyone with a complete live URL can read, edit, and end that activity. **End live sharing** deletes the canonical backend record immediately; browsers that previously opened it retain only their local recovery copies. There are no accounts, participant-level permissions, token rotation, or audit trail in this release. Treat leaked URLs as compromised and avoid regulated or highly sensitive data.

Anonymous RPCs use a secret-peppered request identifier for throttling, and expected invalid input consumes rate-limit budget without being stored. Live activity payloads are validated against bounded input and snapshot sizes before storage. The static host cannot set `frame-ancestors` headers, so the app also refuses to render interactive controls while embedded in another page.
Anonymous RPCs use a secret-peppered request identifier for throttling, and expected invalid input consumes rate-limit budget without being stored. AI calls have separate per-client text/voice quotas plus server-only project-wide daily ceilings. Live activity payloads are validated against the same bounded graph, reference, amount, settlement, timestamp, and snapshot-size rules in the browser and database. Production builds allow connections only to the configured HTTPS Supabase origin. The static host cannot set `frame-ancestors` headers, so the app also refuses to render interactive controls while embedded in another page.

Reports involving capability leakage, RPC privilege escalation, rate-limit bypasses, browser storage exposure, exported summaries, dependency vulnerabilities, or deployment configuration are in scope. Never include a real live activity URL, database password, service-role key, or Supabase access token in a report.
4 changes: 2 additions & 2 deletions docs/AI_EXPENSE_PREVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Typed descriptions use the candidates `google/gemma-4-26b-a4b-it:free` and `goog

A successful provider response that does not satisfy the expense contract is treated as an incomplete conversation: the user receives a localized prompt to restate the amount, payer, and participants. A genuine upstream failure is logged without the expense text and shown as a model-specific retry/manual-entry message. The request has a bounded timeout so an unavailable route cannot leave the user waiting indefinitely.

The server maintains separate cost budgets per normalized client identifier. Text allows 30 requests per 10 minutes and 100 per day; voice allows 10 per 10 minutes and 25 per day. Counters are consumed before the provider call, including provider failures, and the stricter limit wins. OpenRouter account limits remain the hard cost ceiling. Use a preview-only key with a deliberately small limit, but leave enough unused budget for OpenRouter to authorize one worst-case voice request; an almost-exhausted `$0.01` key can reject a recording before the model runs. Never reuse a broad personal key.
The server maintains separate cost budgets per normalized client identifier. Text allows 30 requests per 10 minutes and 100 per day; voice allows 10 per 10 minutes and 25 per day. A second, server-only project ceiling defaults to 500 text and 100 voice provider calls per rolling day, stopping distributed traffic that no single-client quota would catch. Administrators can lower a ceiling or disable one mode in `private.ai_expense_budget_limits`. Counters are consumed before the provider call, including provider failures, and the stricter limit wins. OpenRouter account limits remain the final hard cost ceiling. Use a preview-only key with a deliberately small limit, but leave enough unused budget for OpenRouter to authorize one worst-case voice request; an almost-exhausted `$0.01` key can reject a recording before the model runs. Never reuse a broad personal key.

Each browser also keeps an activity-scoped participant selection. The selected member ID is sent with text and voice requests as `viewerMemberId`, allowing first-person phrases such as “I paid” or “我付的” to resolve to an existing activity member. This selection stays in local storage and is never written into the shared activity snapshot, so collaborators can choose independently on each browser.

Expand Down Expand Up @@ -85,7 +85,7 @@ OPENROUTER_FALLBACK_MODEL=google/gemini-2.5-flash-lite
OPENROUTER_VOICE_MODEL=google/gemini-2.5-flash-lite
```

Keep the production project reference out of the preview deployment environment. The database function `consume_ai_expense_quota` is executable only by the service role used inside the Edge Function; browser clients cannot call it directly.
Keep the production project reference out of the preview deployment environment. The database function `consume_ai_expense_quota_v2` is executable only by the service role used inside the Edge Function; browser clients cannot call it directly or read the private budget table.

If voice recording stops normally but the app reports that its AI budget was reached, check the preview key—not only the account balance—in OpenRouter's **API Keys** page. The key's own cumulative credit limit may be lower than the account balance. Raise that preview-only cap intentionally, then run one short voice request and confirm its cost in OpenRouter Logs.

Expand Down
9 changes: 5 additions & 4 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The production workflow verifies the frontend and database, builds with producti

## Production scope

This is a production-ready trusted-group MVP. Live links are bearer capabilities: anyone with the complete URL can read and edit that activity. Visible live tabs poll for newer revisions every 15 seconds, but there are no accounts, participant-level permissions, audit trail, realtime subscription, or token revocation yet. Do not use this release for regulated, highly sensitive, or adversarial financial data.
This is a production-ready trusted-group MVP. Live links are bearer capabilities: anyone with the complete URL can read, edit, and end that activity. Visible live tabs poll for newer revisions every 15 seconds, but there are no accounts, participant-level permissions, audit trail, realtime subscription, or per-participant revocation. Do not use this release for regulated, highly sensitive, or adversarial financial data.

## One-time setup

Expand Down Expand Up @@ -92,6 +92,7 @@ The workflow can also be started manually from `main` with **Run workflow**.
- Open the link in a private browser, add an expense, and confirm the first visible browser updates automatically within 15 seconds.
- Confirm the recipient receives a persistent `Live · CODE` activity, then go offline and verify that its last synced snapshot remains visible but read-only.
- Choose **Duplicate and edit** while offline and confirm the new independent local copy is editable without changing the Live activity.
- Choose **End live sharing**, confirm the old URL becomes unavailable in another browser, and verify both browsers retain their last synced read-only recovery copy with **Continue locally**.
- Create one local activity and one live activity, then confirm their allowlisted events appear separately in `private.analytics_daily` and `private.analytics_hourly`, and their resolved UI locale appears in `private.analytics_locale_daily`, without URL or activity fields.
- Create one text AI draft and one voice AI draft, then confirm the requested and ready events appear in the **SplitBill - AI Entry Usage** Home report without prompts, audio, or expense fields.
- Run Supabase Security Advisor and Performance Advisor after the first migration.
Expand All @@ -102,11 +103,11 @@ The workflow can also be started manually from `main` with **Run workflow**.
- Backend activities expire 90 days after their last successful update and expired rows are removed incrementally during new activity creation. Each browser that opened the activity keeps its latest full snapshot locally until the person removes it or clears site data; after confirmed backend expiration, that saved copy can continue as a local activity and start a new Live session.
- Create, load, update, and analytics RPCs are rate-limited per secret-peppered identifier derived from the client IP. Rejected requests consume the same budget as successful requests. Review API/database logs and tune limits from observed traffic.
- First-party analytics events expire after 90 days and contain no URL, capability, identity, activity, or financial payload. Review aggregate usage with the queries in [ANALYTICS.md](ANALYTICS.md).
- AI text and voice have separate database quotas. Keep the OpenRouter key limit as the hard cost ceiling and monitor request, clarification, ready, and failure frequency through the privacy-safe Home report.
- AI text and voice have separate per-client database quotas plus server-only rolling project ceilings (500 text and 100 voice provider calls by default). Administrators can lower a ceiling or disable one mode in `private.ai_expense_budget_limits`; keep the OpenRouter key limit as the final hard cost ceiling and monitor request, clarification, ready, and failure frequency through the privacy-safe Home report.
- Free-tier projects should export regular off-site logical backups with `supabase db dump`. Paid projects provide daily backups; consider point-in-time recovery when the recovery objective warrants it. See [Supabase backups](https://supabase.com/docs/guides/platform/backups).
- Review Security Advisor and Performance Advisor after every schema change.
- If a capability URL leaks, treat the activity as compromised. Token rotation/revocation is a required follow-up before serving groups that need stronger access control.
- The build derives the exact Supabase `connect-src` origin from `VITE_SUPABASE_URL`. Missing or unsafe values fall back to the production origin, so preview deployments can use an isolated Supabase project without weakening the policy.
- If a capability URL leaks, use **End live sharing** immediately and create a new Live session from a trusted recovery copy. Tally still has no participant-specific revocation or token rotation.
- The production build derives the exact HTTPS Supabase `connect-src` origin from `VITE_SUPABASE_URL`; missing or unsafe values fall back to the production origin. Local and test endpoints are added only by the explicit browser-test build flag and are absent from release artifacts.

## Rollback

Expand Down
Loading