myspoti
Jetpack Compose로 만든 Spotify 전용 안드로이드 클라이언트
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
Stripe billing with dunning, replay-safe webhooks, idempotent LLM background jobs, PDF/DOCX export (Next.js, Postgres)
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A small but real app. A user answers a short questionnaire about a person's life, an LLM drafts a "Life Story" or a eulogy, the user edits it, and downloads it as PDF and DOCX. Payment is by Stripe: a one-time $9 purchase or a $7/month subscription.
I built it in one weekend to learn and prove three things: Stripe billing with dunning, server-side PDF and DOCX generation, and LLM calls in idempotent background jobs. Stripe runs in test mode only.
Live demo: https://storykeep-one.vercel.app (test card 4242 4242 4242 4242,
any future date, any CVC)
Every claim below links to evidence: a test, a log or a screenshot.
| Topic | What is built | Evidence |
|---|---|---|
| Access control | A user can read only their own stories. Another user's story answers 404. | m1-access-control.md |
| Version history | Answers and story text are append-only. Any version can be restored. | m2-questionnaire-versions.md |
| Background jobs | LLM drafts run in a Postgres job queue with retries and backoff. A retried job never creates a duplicate. Daily cost cap. | m3-llm-jobs.md |
| Replay-safe webhooks | Stripe signature check, plus an events table that makes a replayed event a no-op. | m4-stripe-one-time.md |
| Dunning | active -> past_due -> canceled, a 3 day grace period, a banner and an email. Run with Stripe test clocks. Out-of-order webhooks end in the right state. | dunning.md |
| PDF and DOCX | Built on the server from one content model. Page numbers, no orphan headings, accents and symbols. | m6-export.md, samples |
| Deploy | Vercel and Neon. The job queue runs with no worker process. | DEPLOY.md |
A short list of exactly what I can and cannot claim is in docs/FINAL_REPORT.md.
flowchart LR
Browser["Browser"]
subgraph App["Next.js app"]
Pages["Pages and Server Actions"]
Webhook["/api/stripe/webhook<br/>verifies the signature"]
Export["/api/stories/:id/export<br/>PDF and DOCX"]
end
subgraph DB["PostgreSQL"]
Data[("stories, answers,<br/>story_versions")]
Billing[("orders, subscriptions,<br/>stripe_events")]
Jobs[("jobs, llm_usage")]
end
Worker["Worker<br/>runs jobs"]
Stripe["Stripe<br/>test mode"]
LLM["Anthropic Claude<br/>or mock"]
Mail["SMTP<br/>Mailpit locally"]
Browser --> Pages
Browser -- "download" --> Export
Browser -- "pays on Stripe's page" --> Stripe
Pages -- "create Checkout Session" --> Stripe
Stripe -- "signed webhooks" --> Webhook
Pages --> Data
Pages -- "queue a draft" --> Jobs
Webhook -- "event id once,<br/>then state change" --> Billing
Webhook -- "queue an email" --> Jobs
Export -- "is it paid?" --> Billing
Export --> Data
Worker -- "claim with SKIP LOCKED" --> Jobs
Worker --> LLM
Worker --> Mail
Worker -- "save draft once" --> Data
Three ideas hold the design together:
The subscription state machine:
stateDiagram-v2
[*] --> active: first payment
active --> past_due: renewal payment fails
past_due --> active: a retry succeeds
past_due --> canceled: retries run out
active --> canceled: user cancels
canceled --> [*]
note right of past_due
Access continues for a
3 day grace period.
Banner and one email.
end note
| Part | Choice | Why |
|---|---|---|
| App | Next.js 16 (App Router), TypeScript, Tailwind | |
| Database | PostgreSQL, Drizzle ORM | Drizzle reads like SQL, and the design depends on unique constraints and ON CONFLICT. |
| Auth | Auth.js, email and password | Simple, and easy to drive in browser tests. |
| Jobs | A Postgres table as the queue | No extra service. About 150 lines that I can explain. |
| LLM | Anthropic Claude behind an interface, with a mock | Tests are free and run offline. |
@react-pdf/renderer | Pure Node, so it runs in a serverless function. | |
| DOCX | docx | Real Word structure: heading styles, footer, sections. |
| Tests | Vitest with a real Postgres, Playwright | The database enforces the rules, so the tests use a real one. |
The reasons, trade-offs and "what broke first" for each milestone are in docs/DECISIONS.md.
You need Docker, Node 20 or newer, and pnpm.
git clone https://github.com/samilabud/storykeep.git && cd storykeep
pnpm install
cp .env.example .env.local
# In .env.local set AUTH_SECRET to the output of: openssl rand -base64 32
docker compose up -d # Postgres on 5440, test Postgres on 5441, Mailpit on 8025
pnpm db:migrate
pnpm dev # http://localhost:3100
pnpm worker # second terminal: runs background jobs
This is enough to register, fill the questionnaire and get drafts. The LLM is
a mock by default (LLM_PROVIDER=mock), so no API key is needed.
Measured from a fresh clone on my machine: install, migrate, 191 tests and a production build took 34 seconds (Docker images and the pnpm cache were already present).
To take test payments (optional):
여기까지가 README 앞부분입니다. 나머지는 원문에서 읽을 수 있습니다. README 전문 보기 ↗