Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6088690
Say in the READMEs that pushes are end-to-end encrypted
cdeutsch Oct 27, 2026
ec1eaca
Redesign the front page around what PushThat does and how it is encry…
cdeutsch Oct 27, 2026
e6298ff
Give the site a description and a card for shared links
cdeutsch Oct 27, 2026
642c92b
List everything the server can see on the front page, as the Security…
cdeutsch Oct 27, 2026
5bf87d5
Format the front page's metadata paragraph with Prettier
cdeutsch Oct 27, 2026
913e632
Say that only the Chrome extension opens a push by itself
cdeutsch Oct 27, 2026
b055d7e
Stop promising a few links a day on the Free plan
cdeutsch Oct 27, 2026
d04e05c
Give both plans' file sizes in the front page's feature grid
cdeutsch Oct 27, 2026
11e677c
Fall back to "PushThat" as the tab title on pages that set none
cdeutsch Oct 27, 2026
385e1f8
Keep the site description in `index.html` only
cdeutsch Oct 27, 2026
5c303ec
Correct the comment on where the hero's pill lands
cdeutsch Oct 27, 2026
54260ea
Stop describing a heading highlight the front page does not have
cdeutsch Oct 27, 2026
f629757
Drop the unused step counter from the front page
cdeutsch Oct 27, 2026
5bb711f
Name the front page's muted text shades once
cdeutsch Oct 27, 2026
d3a1f18
Render the front page's two card grids from one component
cdeutsch Oct 27, 2026
c6a1440
Say which part of the share card's copy is the hero's
cdeutsch Oct 27, 2026
493b465
Say that a file is picked, not pasted, when pushing from a computer
cdeutsch Oct 27, 2026
6e21eb3
Say the web works today and the other apps come as they reach the stores
cdeutsch Oct 27, 2026
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@
<link rel="icon" type="image/svg+xml" href="/pushthat.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PushThat</title>
<!--
What a search result and a shared link show, for the crawlers that read the HTML as
served and run nothing. The description lives only here: Helmet adds its tags beside
a static one rather than replacing it, so repeating it in the front page would give
the page two. The front page's tab title, set through Helmet, is og:title's text. The
card is apps/web/og/card.html.
-->
<meta
name="description"
content="Share a link or a file from your phone and it arrives on your computer. Push from the browser and your phone buzzes. End-to-end encrypted with the password you already have. Free to start."
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="PushThat" />
<meta property="og:url" content="https://pushthat.link/" />
<meta property="og:title" content="PushThat: send links and files between your devices, encrypted" />
<meta
property="og:description"
content="Share a link or a file from your phone and it arrives on your computer. Push from the browser and your phone buzzes. End-to-end encrypted with the password you already have. Free to start."
/>
<meta property="og:image" content="https://pushthat.link/og.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<div id="root"></div>
Expand Down
108 changes: 108 additions & 0 deletions apps/web/og/card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!--
The source of public/og.png, the 1200x630 card a shared link shows. It is rendered by
hand rather than at build time so that the build needs no browser; redo it when the
headline changes, from apps/web:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --disable-gpu \
--hide-scrollbars --window-size=1200,630 --screenshot=public/og.png "file://$PWD/og/card.html"

The headline is the hero's; the sentence under it is the card's own and shorter. The
logo is loaded from public/ so there is one copy of it.
-->
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
html,
body {
margin: 0;
width: 1200px;
height: 630px;
overflow: hidden;
}
body {
background: radial-gradient(ellipse at 15% 10%, rgba(80, 46, 168, 0.55), transparent 55%), #1a1a1a;
color: rgba(255, 255, 255, 0.95);
font-family:
Inter,
-apple-system,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 88px;
box-sizing: border-box;
}
.brand {
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 44px;
}
.brand img {
width: 64px;
height: 64px;
}
.brand span {
font-size: 30px;
font-weight: 600;
letter-spacing: -0.01em;
}
h1 {
margin: 0 0 28px;
font-size: 76px;
line-height: 1.04;
font-weight: 700;
letter-spacing: -0.025em;
max-width: 15ch;
}
p {
margin: 0;
font-size: 30px;
line-height: 1.4;
color: rgba(255, 255, 255, 0.78);
max-width: 32ch;
}
.tag {
position: absolute;
right: 88px;
bottom: 56px;
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 24px;
border-radius: 999px;
background: #ffa9e7;
color: #242424;
font-size: 22px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.tag svg {
width: 22px;
height: 22px;
}
</style>
</head>
<body>
<div class="brand"><img src="../public/pushthat.svg" alt="" /><span>PushThat</span></div>
<h1>Send a link to your phone. Or from it.</h1>
<p>
Links and files between your phone and your computer, encrypted on the device with the password you already have.
</p>
<div class="tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
/></svg
>End-to-end encrypted
</div>
</body>
</html>
Binary file added apps/web/public/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions apps/web/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -35,6 +36,13 @@ export default function Layout({ children }: LayoutProps) {

return (
<div className={clsx(styles.layout, isAuthenticated && styles.layoutAuthenticated)}>
{/*
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.
*/}
<Helmet defaultTitle="PushThat" />
<div className={styles.layoutBorder} id="layout-border">
{isAuthenticated && (
<div className={styles.menuContainer}>
Expand Down
Loading
Loading