Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7b21f9d
chore: drop CLI registry endpoint and fix velite-data warning
imnotannamaria May 23, 2026
e41adb1
feat(handler): rename to wristkit-sync, harden the route
imnotannamaria May 23, 2026
aaa865e
fix(card): timezone, hydration, lastSync semantics and palette
imnotannamaria May 25, 2026
df19dd1
feat(web): security headers, MDX error boundary, robust /shortcut
imnotannamaria May 26, 2026
ec52b29
feat(schema): dedupe migration + rename to wristkit-schema
imnotannamaria May 26, 2026
3fbd65c
feat(docs): server-side viewer for the files users copy
imnotannamaria May 26, 2026
c90168c
docs: rewrite the whole docs and home flow for manual copy paste
imnotannamaria May 26, 2026
ffd5e19
fix(a11y): restore keyboard focus rings and add landmarks
imnotannamaria May 26, 2026
44eed89
fix(docs): use source path as the tab key in RegistryFileBundle
imnotannamaria May 28, 2026
95e9073
fix(a11y): move skip link off-screen with top, not transform
imnotannamaria May 28, 2026
0dadbed
fix(shortcut): match actual flat payload from iOS Shortcut
imnotannamaria Jun 9, 2026
6901a03
refactor(today-activity-card): trim copy and drop the partial state
imnotannamaria Jun 9, 2026
b7e5ec8
fix(docs): put the SQL migrations bundle ahead of the route handler
imnotannamaria Jun 9, 2026
d57686a
docs(installation): inline the SQL migrations at step 3
imnotannamaria Jun 9, 2026
f14a958
docs(installation): inline all files and clarify Supabase pooler URL
imnotannamaria Jul 5, 2026
d2dbe4b
fix(registry-files): correct load.ts import and tighten path guard
imnotannamaria Jul 5, 2026
a38354d
chore: sync pnpm-lock.yaml after registry package.json changes
imnotannamaria Jul 5, 2026
96c595b
fix(web): copy shortcut file before dev server so e2e test passes in CI
imnotannamaria Jul 7, 2026
66f473c
docs(readme): rewrite to reflect copy-paste flow, no CLI
imnotannamaria Jul 7, 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
57 changes: 38 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,66 @@

![wristkit hero](./assets/hero.png)

A CLI that drops production-ready React components for visualizing Apple Health data into any Next.js project. You bring your own Supabase. Zero telemetry. MIT.
A set of copy-paste React components for showing Apple Health data in any Next.js project. You bring your own Supabase. Zero telemetry. MIT.

## What it does

- Copy the component files into your project
- Run a SQL migration in Supabase
- Import the iOS Shortcut on your iPhone, set your URL and API key, done
- Your Apple Watch rings show up on your site, live, from your own database — we never see your data

## Quick start

**1. Install the peer dependencies**

```bash
npx wristkit init
pnpm add drizzle-orm postgres zod
```

## What it does
**2. Add your environment variables**

```
WRISTKIT_DATABASE_URL=postgresql://postgres.PROJECT_REF:PASSWORD@aws-0-REGION.pooler.supabase.com:6543/postgres
WRISTKIT_API_KEY=replace-with-32-random-bytes
```

- `npx wristkit init` — detects your Next.js app, writes `components.json` and `.env.local.example`, prints the SQL migration to run in Supabase
- `npx wristkit add today-activity-card` — copies the component files into your project
- Import the iOS Shortcut on your iPhone, edit two fields (URL + API key), done
- Your Apple Watch rings render on your site, live, from your own Supabase — we never see your data
Use the **Transaction pooler** URL from Supabase (Project Settings → Database → Connect). The direct connection does not work on Vercel.

## Quick start
Generate an API key:

```bash
# 1. Initialize
npx wristkit init
node -e 'console.log(require("crypto").randomBytes(32).toString("base64url"))'
```

**3. Run the SQL migration in Supabase**

Go to the [Installation docs](https://wristkit-web.vercel.app/docs/installation) and copy the two SQL blocks into the Supabase SQL Editor.

# 2. Run the SQL migration in your Supabase SQL editor (printed by init)
**4. Copy the component files**

# 3. Add the component
npx wristkit add today-activity-card
Go to the [Installation docs](https://wristkit-web.vercel.app/docs/installation) and copy the route handler and component files into your project.

# 4. Use it in a Server Component
**5. Use it in a Server Component**

```tsx
import { TodayActivityCard, loadTodayActivity } from "@/components/wristkit/today-activity-card"

export default async function Dashboard() {
const state = await loadTodayActivity()
return <TodayActivityCard state={state} />
}

# 5. Set up the iOS Shortcut
npx wristkit shortcut
```

**6. Set up the iOS Shortcut**

Download it from [wristkit.dev/shortcut](https://wristkit-web.vercel.app/shortcut), open it in the Shortcuts app, and fill in your site URL and API key.

## Requirements

- Next.js 15+ (App Router)
- A [Supabase](https://supabase.com) project (free tier works)
- Node.js ≥ 20
- Node.js 20+

## Documentation

Expand All @@ -57,7 +76,7 @@ Full docs at **[wristkit-web.vercel.app/docs](https://wristkit-web.vercel.app/do

## Privacy

wristkit is a CLI + component library. Your data flows directly from your iPhone to your Supabase — we never see it, store it, or have access to it. No analytics SDK. No server-side logging. No third-party cloud.
Your data goes straight from your iPhone to your Supabase. We never see it, store it, or have any access to it. No analytics. No server-side logging. No third-party cloud.

## Development

Expand Down
183 changes: 107 additions & 76 deletions apps/web/app/(marketing)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TodayActivityCardDemo } from "@/components/cards/today-activity-card-demo";
import { Badge } from "@/components/entrepta/badge";
import { Button } from "@/components/entrepta/button";
import { buttonVariants } from "@/components/entrepta/button-variants";
import {
Card,
Expand All @@ -14,28 +13,35 @@ import {
import { TopNav, TopNavLink, TopNavMenu } from "@/components/entrepta/top-nav";
import { HeroIdePreview } from "@/components/home/hero-ide-preview";
import { WristKitMark } from "@/components/mark";
import { SkipLink } from "@/components/skip-link";
import Link from "next/link";

export default function HomePage() {
return (
<>
<SkipLink />
<HomeTopNav />
<main className="page" style={{ paddingTop: 88, paddingBottom: 96 }}>
<main
id="main-content"
tabIndex={-1}
className="page"
style={{ paddingTop: 88, paddingBottom: 96 }}
>
<HeroSection />
<ComponentShowcase />
<PackagesSection />
<InstallSection />
<CtaStrip />
<SiteFooter />
</main>
<SiteFooter />
</>
);
}

// ─── TopNav ───────────────────────────────────────────────────
function HomeTopNav() {
return (
<div
<header
style={{
position: "fixed",
top: 0,
Expand Down Expand Up @@ -113,7 +119,7 @@ function HomeTopNav() {
}
/>
</div>
</div>
</header>
);
}

Expand Down Expand Up @@ -176,9 +182,9 @@ function HeroSection() {
style={{ marginTop: 28, maxWidth: 540, lineHeight: 1.65 }}
>
<strong style={{ color: "var(--fg-primary)", fontWeight: 500 }}>wristkit</strong> is a
small CLI that drops ready to use React components into your Next.js project, so you can
show your Apple Health data on the web. You bring your own Supabase. The iOS Shortcut
posts straight to your endpoint, with no third party cloud and no SDK in the middle.
small set of React components for your Next.js project, so you can show your Apple Health
data on the web. Copy the files from the docs, point them at your own Supabase, and the
iOS Shortcut posts straight to your endpoint. No third party cloud, no SDK in the middle.
</p>
<div
style={{
Expand All @@ -189,11 +195,14 @@ function HeroSection() {
flexWrap: "wrap",
}}
>
<Button variant="command" size="lg">
npx wristkit init
</Button>
<Link
href="/docs/components/today-activity-card"
className={buttonVariants({ variant: "primary", size: "lg" })}
>
browse the components →
</Link>
<Link href="/docs" className={buttonVariants({ variant: "secondary", size: "lg" })}>
read the docs
read the docs
</Link>
</div>
<dl
Expand Down Expand Up @@ -292,22 +301,25 @@ const REGISTRY_ITEMS = [
label: "components/wristkit",
num: "01",
title: "TodayActivityCard",
desc: "A React Server Component with all 7 states (loading, empty, stale, partial, error, ok and rings only). Drop it into any /app page.",
desc: "A React Server Component with all 7 states (loading, empty, stale, partial, error, ok and rings only). Copy it into any /app page.",
tag: "<TodayActivityCard state={state} />",
href: "/docs/components/today-activity-card",
},
{
label: "app/api/healthkit",
label: "app/api/wristkit-sync",
num: "02",
title: "Route handler",
desc: "A POST endpoint that checks the x-api-key, parses the Shortcut payload with Zod and writes to your Supabase through Drizzle.",
tag: "export async function POST(req)",
href: "/docs/installation",
},
{
label: "shortcuts/wristkit",
num: "03",
title: "iOS Shortcut",
desc: "Reads Active Energy, Exercise Minutes and Steps from HealthKit. You can schedule it to run every day at 23:59 with iOS Automation.",
tag: "wristkit-sync.shortcut",
href: "/docs/shortcut-setup",
},
];

Expand Down Expand Up @@ -347,28 +359,34 @@ function PackagesSection() {
}}
>
{REGISTRY_ITEMS.map((p) => (
<Card key={p.num}>
<CardHeader>
<CardLabel>{p.label}</CardLabel>
<CardMeta>{p.num}</CardMeta>
</CardHeader>
<CardTitle>{p.title}</CardTitle>
<p
style={{
fontFamily: "var(--font-sans)",
fontSize: 13,
lineHeight: 1.6,
color: "var(--fg-secondary)",
margin: 0,
}}
>
{p.desc}
</p>
<CardFooter>
<CardComment>{p.tag}</CardComment>
<span style={{ color: "var(--fg-brand)" }}>→</span>
</CardFooter>
</Card>
<Link
key={p.num}
href={p.href}
style={{ display: "block", color: "inherit", textDecoration: "none" }}
>
<Card>
<CardHeader>
<CardLabel>{p.label}</CardLabel>
<CardMeta>{p.num}</CardMeta>
</CardHeader>
<CardTitle>{p.title}</CardTitle>
<p
style={{
fontFamily: "var(--font-sans)",
fontSize: 13,
lineHeight: 1.6,
color: "var(--fg-secondary)",
margin: 0,
}}
>
{p.desc}
</p>
<CardFooter>
<CardComment>{p.tag}</CardComment>
<span style={{ color: "var(--fg-brand)" }}>→</span>
</CardFooter>
</Card>
</Link>
))}
</div>
</section>
Expand All @@ -379,17 +397,22 @@ function PackagesSection() {
const INSTALL_STEPS = [
{
n: "01",
cmd: "npx wristkit init",
out: "components.json written · .env.local.example written",
cmd: "copy → components/wristkit/today-activity-card",
out: "card, states, load and queries",
},
{
n: "02",
cmd: "npx wristkit add today-activity-card",
out: "3 files copied · 4 deps installed",
cmd: "copy → app/api/wristkit-sync/route.ts",
out: "ingest handler · zod + drizzle",
},
{
n: "03",
cmd: "open wristkit.vercel.app/shortcut",
cmd: "psql -f schemas/0001_initial.sql",
out: "supabase sql editor",
},
{
n: "04",
cmd: "open wristkit-web.vercel.app/shortcut",
out: null,
},
];
Expand All @@ -414,13 +437,13 @@ function InstallSection() {
· getting started
</div>
<h2 className="t-display-md" style={{ margin: "0 0 16px", fontSize: 52, lineHeight: 1.05 }}>
<em className="t-italic">Three</em> commands.
<em className="t-italic">Four</em> files.
<br />
<span className="t-muted">You own the pipeline.</span>
</h2>
<p className="t-body-md t-secondary" style={{ maxWidth: 540, lineHeight: 1.65, margin: 0 }}>
Install the CLI, wire up the API route and set the Apple Shortcut to run once a day.
Snapshots land in your Supabase and your React renders them.
Copy the component into your project, paste the route handler, run the SQL on Supabase and
install the Apple Shortcut. Snapshots land in your database and your React renders them.
</p>
</div>
<div
Expand Down Expand Up @@ -643,9 +666,12 @@ function CtaStrip() {
flexWrap: "wrap",
}}
>
<Button variant="command" size="lg">
npx wristkit init
</Button>
<Link
href="/docs/components/today-activity-card"
className={buttonVariants({ variant: "primary", size: "lg" })}
>
browse the components →
</Link>
<Link href="/docs" className={buttonVariants({ variant: "secondary", size: "lg" })}>
read the docs
</Link>
Expand Down Expand Up @@ -750,35 +776,40 @@ function SiteFooter() {
gap: 8,
}}
>
{group.items.map((item) => (
<li key={item.label}>
{item.external ? (
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
style={{
fontFamily: "var(--font-mono)",
fontSize: 12,
color: "var(--fg-secondary)",
}}
>
{item.label}
</a>
) : (
<Link
href={item.href}
style={{
fontFamily: "var(--font-mono)",
fontSize: 12,
color: "var(--fg-secondary)",
}}
>
{item.label}
</Link>
)}
</li>
))}
{group.items.map((item) => {
const cleanLabel = item.label.replace(/\s*↗\s*$/, "");
return (
<li key={item.label}>
{item.external ? (
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
style={{
fontFamily: "var(--font-mono)",
fontSize: 12,
color: "var(--fg-secondary)",
}}
>
{cleanLabel}
<span aria-hidden> ↗</span>
<span className="sr-only"> (opens in new tab)</span>
</a>
) : (
<Link
href={item.href}
style={{
fontFamily: "var(--font-mono)",
fontSize: 12,
color: "var(--fg-secondary)",
}}
>
{cleanLabel}
</Link>
)}
</li>
);
})}
</ul>
</div>
))}
Expand Down
Loading
Loading