Phase 1 foundation: backend, web profile, and mobile app
CI / policy (push) Successful in 0s
CI / mobile (push) Successful in 47s
CI / profile (push) Successful in 49s
CI / backend (push) Failing after 49s

Greenfield implementation of CardClaws Phase 1 across three surfaces.

Backend (Rust/Axum workspace, 67 tests):
- cardclaws-types/config/db/auth/api/wallet crates
- Auth: register, login + lockout, magic link, refresh rotation, Apple verify
- Cards: CRUD, tier-limited publish, duplicate, public handle lookup
- Assets: R2 presigned uploads; vCard export
- Apple Wallet .pkpass pipeline (PKCS#7 signer behind apple-signing feature)
- Analytics ingest + summary with daily-salted IP hashing
- Migrations 0001 (incl. cardclaws_sessions) + 0002 analytics

Web profile (Astro SSR): cardclaws.com/[handle] hero + flip, contact actions,
client-built vCard, visit attribution. Verified end-to-end.

Mobile (Expo SDK 51): auth, card list/create, builder v1 (bg/text/logo,
palette, undo/redo), Skia/Reanimated viewer (flip + ambient). 22 logic tests.

CI: policy/backend/profile/mobile jobs; LOC + no-placeholder lint.

Review fixes baked in: `back` (not `cardclaws`) key; strip image is a bundled
manifest file (not a URL); sessions table added; NFC reframed; test doubles
allowed for external services.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-04 10:17:26 -05:00
co-authored by Claude Opus 4.8
commit c30d3afeec
128 changed files with 36279 additions and 0 deletions
@@ -0,0 +1,68 @@
-- 0001_initial.sql — core tables (PRD §9.3) plus the cardclaws_sessions table
-- that §9.3 omitted but §6.8.1/§20.1 require.
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
handle TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
tier TEXT NOT NULL DEFAULT 'free'
CHECK (tier IN ('free', 'pro', 'team', 'enterprise')),
-- Argon2id PHC string. NULL for accounts created purely via OAuth/magic link.
password_hash TEXT,
avatar_r2_key TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE cards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
handle TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'active', 'archived')),
definition JSONB NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_cards_owner ON cards(owner_id);
CREATE INDEX idx_cards_handle ON cards(handle) WHERE status = 'active';
-- Refresh-token sessions. The refresh token itself is never stored; only a
-- SHA-256 hash so a DB leak cannot be replayed. Rotated on every use (§20.1).
CREATE TABLE cardclaws_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
refresh_token_hash TEXT NOT NULL UNIQUE,
device_fingerprint TEXT,
last_active_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sessions_user ON cardclaws_sessions(user_id);
CREATE TABLE share_links (
token TEXT PRIMARY KEY,
card_id UUID NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
modality TEXT NOT NULL,
campaign TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ
);
CREATE INDEX idx_share_links_card ON share_links(card_id);
CREATE TABLE wallet_registrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
card_id UUID NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
platform TEXT NOT NULL CHECK (platform IN ('apple', 'google')),
device_library_id TEXT,
push_token TEXT,
registered_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_wallet_reg_card ON wallet_registrations(card_id);
@@ -0,0 +1,28 @@
-- 0002_analytics.sql — raw event log and hourly rollups (PRD §9.3 / §18).
-- Added in B2 so the analytics write path (B4) has a home before week 7.
CREATE TABLE analytics_events (
id BIGSERIAL PRIMARY KEY,
card_id UUID NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
share_token TEXT REFERENCES share_links(token),
-- SHA-256(ip + per-day salt). Never raw IP (§18.3).
ip_hash TEXT,
country TEXT,
city TEXT,
user_agent TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_analytics_card_time ON analytics_events(card_id, occurred_at DESC);
CREATE TABLE analytics_rollups_hourly (
card_id UUID NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
hour TIMESTAMPTZ NOT NULL,
visits INTEGER NOT NULL DEFAULT 0,
qr_scans INTEGER NOT NULL DEFAULT 0,
nfc_taps INTEGER NOT NULL DEFAULT 0,
saves INTEGER NOT NULL DEFAULT 0,
link_clicks INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (card_id, hour)
);