diff --git a/README.md b/README.md
index b9aebc2..0b98306 100644
--- a/README.md
+++ b/README.md
@@ -28,9 +28,11 @@ The apps and the Firestore and Storage rules are what decide what a client can r
write, and they are all here: [apps/web/firestore.rules](apps/web/firestore.rules) and
[apps/web/storage.rules](apps/web/storage.rules), with their tests beside them. The Cloud
Functions are here too, but nothing proves that what runs on pushthat.link is this code, so
-that part you take on trust. Pushes are not end-to-end encrypted today: Firebase holds them
-as you sent them. [End-to-end encryption](docs/e2ee.md) is the design for changing that,
-and none of it has shipped yet.
+that part you take on trust. Pushes are end-to-end encrypted: a push's link, title and file
+are encrypted on the device with a key made from the account password, and Firebase holds
+only ciphertext and the metadata it needs to count, deliver and expire it. [End-to-end
+encryption](docs/e2ee.md) is the specification, and `packages/push-test` is the reference
+the five clients are checked against.
## Layout
diff --git a/apps/web/index.html b/apps/web/index.html
index 7562875..24a8b15 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -5,6 +5,29 @@
+ Links and files between your phone and your computer, encrypted on the device with the password you already have.
+
+
+ End-to-end encrypted
+
+
+
diff --git a/apps/web/public/og.png b/apps/web/public/og.png
new file mode 100644
index 0000000..cb1ee01
Binary files /dev/null and b/apps/web/public/og.png differ
diff --git a/apps/web/src/Layout.tsx b/apps/web/src/Layout.tsx
index b79dd95..b980ca1 100644
--- a/apps/web/src/Layout.tsx
+++ b/apps/web/src/Layout.tsx
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import { ReactNode, useEffect, useState } from 'react';
+import { Helmet } from 'react-helmet';
import { useLocation, useNavigate } from 'react-router';
import { useAuth } from '@/contexts/AuthContext';
@@ -35,6 +36,13 @@ export default function Layout({ children }: LayoutProps) {
return (
+ {/*
+ react-helmet sets document.title only while some Helmet gives one, and does not put
+ the old title back when that Helmet unmounts. Without this fallback, a page with no
+ title of its own (the login page, the list) keeps the title of the last page that
+ had one, which is usually the front page's long one.
+ */}
+
+ );
+}
+
+function HowItWorks() {
+ const steps = [
+ {
+ title: 'Sign in everywhere',
+ body: 'One account on your phone, your computer and Chrome. Your password unlocks the key on each device, and it never leaves them.',
+ },
+ {
+ title: 'Share it',
+ body: 'Tap PushThat in the share sheet on your phone. On a computer, paste a link or pick a file in PushThat on the web or in the Chrome extension. A link, a page, a photo, a PDF.',
+ },
+ {
+ title: 'It is already there',
+ body: 'With the Chrome extension, a link opens in a new tab on your computer. A push from the browser arrives on your phone as a notification. Every push lands in one searchable list on every device.',
+ },
+ ];
+ return (
+
+
How it works
+
+ {steps.map((step, i) => (
+
+ {i + 1}
+
{step.title}
+
{step.body}
+
+ ))}
+
+
+ );
+}
+
+/** The reverse direction, so that nobody reads the hero as "phone to computer" only. */
+function BothDirections() {
+ return (
+
+
+
Both directions
+
+ Push a page from Chrome or the web app and your phone buzzes. Open the notification and the link is there, in
+ the browser you use on the phone. Found something on the phone instead? Share it, and it is a tab in Chrome
+ before you have reached the keyboard.
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+interface Card {
+ Icon: typeof KeyIcon;
+ title: string;
+ body: string;
+}
+
+/** The icon-title-sentence grid the encryption section and the features share. */
+function CardGrid({ cards }: { cards: Card[] }) {
+ return (
+
+ {cards.map(({ Icon, title, body }) => (
+
+
+
{title}
+
{body}
+
+ ))}
+
+ );
+}
+
+function Encryption() {
+ const points = [
+ {
+ Icon: KeyIcon,
+ title: 'Your password is the key',
+ body: 'The app turns your password into the key on the device. There is no second passphrase to set up and nothing to switch on: it is on for every push, from the first.',
+ },
+ {
+ Icon: MagnifyingGlassIcon,
+ title: 'Search without reading',
+ body: 'Search runs on scrambled tokens made with your key, so the server can match a search without ever seeing the words.',
+ },
+ {
+ Icon: ShieldCheckIcon,
+ title: 'A recovery key, shown once',
+ body: 'Forget your password and the recovery key keeps your history. Without it, nobody can read the old pushes, and that includes us.',
+ },
+ ];
+ return (
+
+
End-to-end encrypted
+
Encrypted before it leaves your hand.
+
+ A push’s link, its title, a file’s name and its contents are encrypted on your device and opened only on the
+ devices you sign in to. Our servers, Google’s, and anyone with access to either hold only what they cannot read.
+
+
+
+ What we can still see: which account pushed, when, whether it was a link or a file, roughly how
+ big a file is, which of your devices sent it and how many pushes you make. Search also shows us when two of your
+ pushes share a word, though not what the word is. The monthly limits, file expiry and notifications need these.
+ The design is published, so you can check it rather than take our word for it.
+
+
+ );
+}
+
+function Features() {
+ const features = [
+ {
+ Icon: BoltIcon,
+ title: 'Opens before you get there',
+ body: 'The Chrome extension opens a pushed link in a new tab the moment it arrives. No click, and no site tab left open to do it.',
+ },
+ {
+ Icon: BellAlertIcon,
+ title: 'Your phone, from the browser',
+ body: 'Push a page from Chrome or the web app and it arrives on your phone as a notification that opens the link.',
+ },
+ {
+ Icon: DocumentArrowUpIcon,
+ title: 'Files too',
+ body: `Photos, PDFs, anything up to ${formatMb(LIMITS.pro.fileBytes)} on Pro (${formatMb(LIMITS.free.fileBytes)} on Free). Stored under a random name, encrypted like everything else, and gone when they expire.`,
+ },
+ {
+ Icon: MagnifyingGlassIcon,
+ title: 'One list, searchable',
+ body: 'Everything you have pushed, on every device, with search. Links stay until you delete them.',
+ },
+ {
+ Icon: DevicePhoneMobileIcon,
+ title: 'Every device you own',
+ body: 'The web today, then Android, iPhone, Mac and a Chrome extension as each reaches its store. The same account and the same list on all of them.',
+ },
+ {
+ Icon: CodeBracketIcon,
+ title: 'Source available',
+ body: 'The apps, the rules that guard the database and the encryption design are public. Read them, build them, or run your own.',
+ },
+ ];
+ return (
+
+
What it does
+
+
+ );
+}
/** The two tiers side by side (docs/billing.md at the repository root). The numbers are the functions' and the rules'. */
function Pricing() {
return (
-
-
Pricing
+
+
Pricing
+
Free is enough for a link or two most days. Pro is for people who live in it.
Free
@@ -20,6 +221,9 @@ function Pricing() {
Files up to {formatMb(LIMITS.free.fileBytes)}
Files kept {LIMITS.free.keepDays} days
+
+ Sign up free
+
Pro
@@ -32,6 +236,9 @@ function Pricing() {
Files up to {formatMb(LIMITS.pro.fileBytes)}
Files kept {LIMITS.pro.keepDays} days
+
+ Sign up, then upgrade
+
@@ -45,71 +252,60 @@ function Pricing() {
export function IndexPage() {
return (
+
+ PushThat: send links and files between your devices, encrypted
+
+
-
-
PushThat
-
The easiest way to send links and files between Android and your other devices.
+ PushThat moves links and files between your phone and your computer. Share from Android or iPhone and it
+ opens in Chrome before you reach the keyboard, and waits in the list on the web and on your Mac. Everything
+ is encrypted on the device with the password you already have, so nobody in between can read it. Including
+ us.
+
+ Free plan, no card. The web today; Android, iPhone, Mac and Chrome as they reach the stores.
+
+
-
-
-
-
-
-
- Android to Desktop
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Desktop to Android
-
-
-
-
-
-
-
-
+
+
+
+
);
}
-
-
diff --git a/docs/README.md b/docs/README.md
index 31736e4..6e7c211 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -13,5 +13,5 @@ house style ([AGENTS.md](../AGENTS.md)).
and what stays manual.
- [Billing](billing.md): the plan, the Stripe accounts, and how the functions and the rules
enforce it.
-- [End-to-end encryption](e2ee.md): the specification for encrypting pushes. A design; none
- of it has shipped yet.
+- [End-to-end encryption](e2ee.md): the specification for encrypting pushes, which every
+ client implements and the rules require.