Files
petfeed/.env.example
T
adminandClaude Sonnet 5 c6ffc1336e feat(sponsor): Stripe-based voluntary sponsoring with Sponsor badge (Gitea #22)
Replaces the (never-launched) ad business direction with a freiwilliger
Beitrag/Community-Abo model: owners can support PawFeed via Stripe with
a one-time payment or a monthly SEPA/card subscription (2 EUR minimum),
declared as Sponsoring/Community-Abo, not a tax-deductible donation. The
Sponsor badge (only perk, no feature unlocks) is Owner-scoped and shows
on every post of every one of their pets, plus their profile.

- Schema: Owner.stripeCustomerId/sponsorSince/sponsorPeriodEnd/
  sponsorSubscriptionId/sponsorSubscriptionStatus, new
  SponsorContribution model (payment history)
- src/lib/stripe.ts (singleton client, mux.ts pattern) + src/lib/sponsor.ts
  (isActiveSponsor: one-time givers keep the badge permanently, subscribers
  through sponsorPeriodEnd even after cancelling -- no mid-period cliff)
- src/trpc/routers/sponsor.ts: getStatus, createCheckoutSession (SEPA +
  card for subscriptions, card for one-time, price_data since amounts are
  user-chosen), cancelSubscription (cancel_at_period_end, no hard stop)
- src/app/api/webhooks/stripe: checkout.session.completed, invoice.paid,
  customer.subscription.updated/deleted (mux webhook route pattern)
- src/app/(app)/unterstuetzen: amount chips (3/5/10 EUR) prefilling an
  editable field, one-time/subscription toggle, status + contribution
  history, in-app cancel button (no Stripe-hosted portal, per requirement)
- Badge surfaced in feed/explore postInclude + PostCard, and on the pet
  profile page next to the existing isVerified badge
- Nutzungsbedingungen Sec. 12 (draft, needs sign-off before launch) +
  Stripe added to the Datenschutzerklaerung's Auftragsverarbeiter list
- src/__tests__/sponsor.test.ts (7 tests), Stripe client fully mocked

STRIPE_WEBHOOK_SECRET intentionally not yet configured -- the webhook
endpoint can only be registered in the Stripe dashboard once this is
deployed and reachable at https://pawfeed.org/api/webhooks/stripe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 10:24:08 +02:00

96 lines
6.1 KiB
Bash

# ============================================================
# OnlyPets — Environment Variables
# Copy this file to .env.local and fill in your values.
# NEVER commit .env.local to version control.
# ============================================================
# ── Supabase ─────────────────────────────────────────────────
# Supabase Dashboard -> Project Settings -> Database -> Connection string
# DATABASE_URL: POOLED connection (port 6543, pgbouncer) — used by Prisma at runtime
# DIRECT_URL: DIRECT connection (port 5432) — used by Prisma for migrations
DATABASE_URL="postgresql://postgres.xxxxxxxxxxxx:password@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://postgres.xxxxxxxxxxxx:password@aws-0-eu-central-1.pooler.supabase.com:5432/postgres"
# Supabase Dashboard -> Project Settings -> API
# NEXT_PUBLIC_SUPABASE_URL: Project URL (e.g. https://xxxx.supabase.co)
# !! This is NOT the database URL — it is the Supabase REST/Storage base URL !!
# Used for: avatar CDN links, signed upload URLs, Storage client.
# Baked into the client bundle at build time (NEXT_PUBLIC_ prefix).
# SUPABASE_SERVICE_ROLE_KEY: service_role key (secret! — server-only, never expose to client)
NEXT_PUBLIC_SUPABASE_URL="https://xxxxxxxxxxxxxxxxxxxx.supabase.co"
SUPABASE_SERVICE_ROLE_KEY="eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ── Clerk (Authentication) ───────────────────────────────────
# Clerk Dashboard -> API Keys
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_xxxxxxxxxxxxxxxxxxxx"
CLERK_SECRET_KEY="sk_test_xxxxxxxxxxxxxxxxxxxx"
# ── Stripe (Sponsoring, Gitea #22) ──────────────────────────
# Stripe Dashboard → Developers → API keys
STRIPE_SECRET_KEY="sk_test_xxxxxxxxxxxxxxxxxxxxxxxx"
# Stripe Dashboard → Developers → Webhooks → Add endpoint → copy Signing Secret
# Endpoint must point at https://<your-domain>/api/webhooks/stripe
# Subscribe to: checkout.session.completed, invoice.paid,
# customer.subscription.updated, customer.subscription.deleted
STRIPE_WEBHOOK_SECRET="whsec_xxxxxxxxxxxxxxxxxxxxxxxx"
# ── Mux (Video) ──────────────────────────────────────────────
# Mux Dashboard → Settings → Access Tokens → Create Token (Mux Video)
MUX_TOKEN_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
MUX_TOKEN_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Mux Dashboard → Webhooks → Add endpoint → copy Signing Secret
# Endpoint must point at https://<your-domain>/api/webhooks/mux
MUX_WEBHOOK_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Your public app URL — currently informational only. Mux's cors_origin
# (videos.ts) is intentionally "*" for now; restrict it to this value if
# you lock down direct-upload origins later.
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# ── Cron ──────────────────────────────────────────────────────
# Bearer token required by /api/cron/trim-feeds and /api/cron/anniversaries.
# vercel.json's crons array is inert on this self-hosted deployment — an
# external trigger (e.g. a NAS-side crontab) must call these routes with
# this token in the Authorization header.
CRON_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ── Admin Panel ───────────────────────────────────────────────
# NEVER use NEXT_PUBLIC_ prefix — these MUST stay server-only!
# ADMIN_SECRET: random UUID path segment (/p/[secret]/) — generate with:
# node -e "console.log(require('crypto').randomUUID())"
ADMIN_SECRET="your-random-uuid-here"
# ADMIN_OWNER_ID: Your Clerk user ID (Clerk dashboard). Bootstraps the super-admin.
ADMIN_OWNER_ID="user_xxxxxxxxxxxxxxxxxxxx"
# ── Redis ─────────────────────────────────────────────────────
# Self-hosted: use the Docker Redis service (docker-compose sets this automatically).
# Local dev: run `docker run -d -p 6379:6379 redis:7-alpine` or install Redis locally.
REDIS_URL="redis://localhost:6379"
# ── Invite Gate ───────────────────────────────────────────────
# Set to "false" to disable the invite requirement after the Alpha/Beta phase.
# Default: "true" (invite required — enforced in proxy.ts)
INVITE_REQUIRED="true"
# ── Error Monitoring (Sentry) ───────────────────────────────────
# Sentry Dashboard -> Settings -> Projects -> <project> -> Client Keys (DSN)
# Same DSN value in both — server-side (src/instrumentation.ts) and
# client-side (src/instrumentation-client.ts) report to the same project.
# Leave unset to run with Sentry fully inert (no build-time requirement).
SENTRY_DSN="https://xxxxxxxxxxxxxxxxxxxx@o0.ingest.sentry.io/0"
NEXT_PUBLIC_SENTRY_DSN="https://xxxxxxxxxxxxxxxxxxxx@o0.ingest.sentry.io/0"
# Optional — only needed to upload source maps for readable stack traces.
# Sentry Dashboard -> Settings -> Auth Tokens (needs project:releases scope).
SENTRY_ORG="your-sentry-org"
SENTRY_PROJECT="your-sentry-project"
SENTRY_AUTH_TOKEN=""
# ── SMTP (Team mailbox) ──────────────────────────────────────────
# Use a mailbox hosted by an established provider (e.g. your domain host's
# mail hosting) — NOT a self-hosted relay from a home/dynamic IP, which gets
# spam-flagged by most receiving servers regardless of DKIM/SPF.
SMTP_HOST="smtp.example.com"
SMTP_PORT="465"
SMTP_USER="team@example.com"
SMTP_PASSWORD=""
MAIL_FROM="team@example.com"