Fields & Floors Collectors
eBay에서 번호가 매겨진 스포츠 카드들을 인쇄본·사인·신인 여부·가격으로 검색하고, 검색을 저장해 새 카드가 등록되면 이메일 알림을 받는다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Valice Press — storefront (formerly "Digital Bookstore")
A first-party, B2C digital bookstore with Social DRM.
Buy a book once, download a watermarked PDF, and read it anywhere — yours to keep.
Valice Press is a first-party publisher; this repository is its production storefront for selling DRM-free
PDFs of books we own or license. It is deliberately first-party (no
multi-vendor marketplace), one-time purchase (no subscription), and
Social-DRM-protected (per-buyer watermark stamped into every download,
no hard-DRM file locks).README 전문 보기 ↗A first-party, B2C digital bookstore with Social DRM. Buy a book once, download a watermarked PDF, and read it anywhere — yours to keep.
Valice Press is a first-party publisher; this repository is its production storefront for selling DRM-free PDFs of books we own or license. It is deliberately first-party (no multi-vendor marketplace), one-time purchase (no subscription), and Social-DRM-protected (per-buyer watermark stamped into every download, no hard-DRM file locks).
The customer-facing surface is a calm, typography-forward storefront optimized for organic discovery. The operator-facing surface is a self-contained admin dashboard that handles catalog management, order visibility, and book lifecycle (draft → published → archived) without raw SQL. The fulfillment pipeline is idempotent and observability- covered, so the same Paddle webhook hitting our endpoint a hundred times produces exactly one watermarked PDF, one entitlement, and one "your book is ready" email.
| Layer | Choice | Why |
|---|---|---|
| Frontend framework | Next.js 16 (App Router) | SSG/ISR-first for catalog SEO; Server Actions for mutations |
| Language | TypeScript (strict) | Compile-time safety across the data + UI layers |
| Styling | Tailwind CSS v4 + shadcn/ui (new-york style) | Token-driven design system; calm-literary tone |
| Database | Neon (serverless Postgres) | WebSocket transactions for atomic fulfillment; branches per env |
| ORM | Drizzle + drizzle-kit | Strict types, migration-first, no runtime bloat |
| Authentication | Clerk (@clerk/nextjs) | Hosted identity (magic-link + social); session JWT at the edge |
| Object storage | Cloudflare R2 | $0 egress for sustained PDF downloads (S3-compatible API) |
| Payments | Paddle (Merchant of Record) | Offloads global VAT / PCI / chargeback liability |
| Async jobs | Inngest | Durable, retry-aware steps for the watermark pipeline |
| Rate limit + cache | Upstash Redis | Sliding-window limiter + Data Cache backend |
Resend + @react-email/components | Transactional "your book is ready" notifications | |
| Observability | Sentry + Vercel Analytics + Speed Insights | Errors, performance, Core Web Vitals |
| Testing | Vitest + @testing-library/react + jsdom | Co-located unit tests; CI-friendly |
| pdf-lib (server stamping) + pdf.js (online reader) | Same-origin worker, CSP-strict | |
| Hosting | Vercel (Fluid Compute) | Native Next.js, edge-aware deploys, ISR everywhere |
The catalog routes — home, /books, /books/[slug], /categories/[slug],
/authors/[slug], /blog, /blog/[slug], /blog/category/[slug],
/sitemap.xml — are all statically rendered (○ Static or ● SSG)
with 1-hour ISR. Auth-gated routes (/account/*, /admin/*, /read/*,
/order/[id], /cart, /search) are ƒ Dynamic.
The split is enforced at every SUB-PR: every change is verified against the build's route-classification table to confirm no static page silently fell over to dynamic. The growth engine (organic search → catalog page → checkout) sits entirely on cached HTML; auth-gated surfaces pay the dynamic cost only when actually used.
Paddle webhook
│ (HTTPS, signature-verified)
▼
/api/webhooks/paddle/route.ts
│ 1. SDK signature unmarshal — bad sig → 401, no DB writes
│ 2. processCompletedTransaction()
│ ├─ orders UPSERT (mor_order_ref UNIQUE) ← DB lock layer
│ ├─ order_items INSERT
│ └─ entitlements UPSERT (user_id, book_id UNIQUE) ← DB lock layer
│ 3. inngest.send("fulfillment.transaction.completed")
▼
Inngest queue
│
▼
processFulfillment function (src/inngest/functions/watermark.ts)
│ for each bookId:
│ step.run("watermark-${bookId}") ← Inngest checkpoint layer
│ ├─ short-circuit if entitlement.status === "ready" ← DB-state layer
│ ├─ fetch master from R2
│ ├─ stamp watermark (name + order ref, no email PII)
│ ├─ upload artifact to R2
│ └─ entitlement → "ready"
│ step.run("email-order-ready-${bookId}")
│ └─ Resend send with idempotencyKey="order-ready:${orderId}:${bookId}"
│ ← Resend dedupe layer
▼
Customer receives exactly one email per book; downloads via signed URL
Three layers of fulfillment idempotency (DB UNIQUE → Inngest step name →
DB-state short-circuit) plus two layers of email idempotency (Inngest
step + Resend idempotencyKey) mean the same Paddle event can hit our
endpoint a hundred times and produce exactly one of everything.
Every external SDK (Paddle, R2, Resend, Sentry, Upstash, Inngest) is
constructed on first call, not at module load. Missing env vars do
not crash next build, npx tsc, or npm run dev. The unhappy
path is:
"R2 storage is not configured. Set R2_ENDPOINT, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY.")safeQuery / { ok } discriminated
returns / loadAdminContext / <UnprovisionedNotice> so the user
sees a calm "configuration required" page instead of a 500Translated: a freshly-cloned repo with zero env vars set still boots, renders public pages with graceful empty states, and tells the operator exactly what to provision.
The complete operational blueprint for every third-party service — Neon, Clerk, Cloudflare R2, Paddle, Inngest, Upstash, Resend, Sentry, Vercel Analytics — is in:
KURULUM_VE_ENV_REHBERI.md(Turkish — Kurulum ve Ortam Değişkenleri Rehberi)
That document is grounded in actual process.env.* references from
the running code (scanned, not guessed) and covers exact dashboard
paths for each provider, three-environment placement (local / Vercel
Preview / Vercel Production), troubleshooting, and a production
go-live checklist. Treat it as the source of truth for env wiring.
# 1. Install dependencies
npm install
# 2. Copy the env schema and fill in values per KURULUM_VE_ENV_REHBERI.md
cp .env.example .env.local
# 3. Apply DB schema (after DATABASE_URL is set)
npm run db:migrate
# 4. Dev server
npm run dev
# 5. (Optional) Inngest dev runner in a second terminal
npx inngest-cli@latest dev
The site boots on http://localhost:3000 even with no env vars set
— public pages render with empty states, dynamic pages render the
"configuration required" notice.
npm run lint # ESLint
npx tsc --noEmit # TypeScript
npm run build # Production build + route classification table
npm run test # Vitest (currently 25 tests across 3 files)
npm run test:watch # Vitest in watch mode
All four pass on a clean clone.
The project shipped across five disciplined phases, 17 SUB-PRs total.
Each SUB-PR has a report in sub-pr-report/ documenting decisions,
trade-offs, and architectural invariants.
| Phase | Theme | SUB-PRs |
|---|---|---|
| 0 — Foundations | Repo scaffold, design tokens, security headers, CI, DB, storage, auth, admin ingest | 0.1 — 0.6 |
| 1 — Commerce core | SSG catalog, full-text search, sample viewer, cart, checkout (Paddle), watermark pipeline, library/order UX | 1.1 — 1.7 |
| 2 — Reading & accounts | PDF.js reader, reading-progress sync, account/orders/GDPR | 2.1 — 2.3 |
| 3 — Discovery & growth | SEO hardening + structured data, blog content hub, reviews + AggregateRating | 3.1 — 3.3 |
| 4 — Operations & optimization | Admin dashboard, rate limiting + caching, transactional email + analytics, catalog management + publish flow, Sentry observability + Vitest scaffolding | 4.1 — 4.5 |
The constitution that governs the project lives in
memory/PAST_DECISIONS.md — locked
architectural decisions, explicit rejections, and re-open triggers.
src/
├─ app/ # Next.js App Router — routes + layouts
│ ├─ admin/ # /admin dashboard + /admin/books/[slug]/edit
│ ├─ api/ # Webhook + cart-count + Inngest endpoints
│ ├─ blog/ # Content hub (markdown-backed, SSG)
│ ├─ books/ # Catalog browse + detail (SSG + ISR)
│ ├─ account/ # Library, orders, settings (Dynamic, auth-gated)
│ ├─ read/ # In-browser PDF reader (Dynamic)
│ └─ ...
├─ components/ # Reusable UI (mix of Server + Client)
├─ content/blog/ # Markdown blog posts (deploy-pinned)
├─ emails/ # React Email templates
├─ inngest/functions/ # Async workers (watermark + email)
├─ lib/
│ ├─ db/ # Drizzle schema, client, queries
│ ├─ storage/ # R2 S3-compatible client (lazy init)
│ ├─ auth.ts # Clerk helpers + requireAdmin gate
│ ├─ cart.ts # Cookie-backed cart (safeParseCart)
│ ├─ email.ts # Resend client + sendOrderReadyEmail
│ ├─ logger.ts # Structured logger → Sentry bridge
│ ├─ rate-limit.ts # Upstash sliding window
│ ├─ seo.ts # JSON-LD + canonical URL helpers
│ └─ ...
sentry.client.config.ts # Sentry browser init
sentry.server.config.ts # Sentry Node init
sentry.edge.config.ts # Sentry Edge init
instrumentation.ts # Next.js register() hook
src/proxy.ts # Clerk middleware + rate limiter (Next 16 rename of middleware.ts)
drizzle/ # Generated SQL migrations (committed)
memory/ # Project constitution + agent memory
sub-pr-report/ # Per-SUB-PR engineering reports
KURULUM_VE_ENV_REHBERI.md # Turkish provisioning guide (17 sections)
MIT.
This README is the front door. The deep operational details live in
KURULUM_VE_ENV_REHBERI.md; the
architectural constitution lives in
memory/PAST_DECISIONS.md; per-SUB-PR
engineering decisions live in sub-pr-report/.