# Szabadpálya — spec overview **Status:** draft v0.7 — 2026-08-22 **Working name:** Szabadpálya · szabadpalya.hu **Reference UX:** reward-based crowdfunding, Kickstarter pattern --- ## 1. Positioning Curated, invite-only crowdfunding platform for public projects: helyi szervezetek, Lepke-típusú kulturális egyesületek, szövetséges NGO-k. **Party-adjacent, not party-branded.** The platform has its own name, identity and operating entity. Momentum's name, logo and colours do not appear in the product, the domain, or the legal footer. The connection is in who gets approved and who founded it — not in the branding. Practical consequences: - Separate legal operator: a **new egyesület**, to be founded (see 1c). - Own name and mark (see 1b). The visual *world* is shared with the Momentum **rendszerbontó** identity — palette, typography, motif — but the Momentum mark and wordmark never appear. Shared language, separate signature. - Impressum names the egyesület. No "a Momentum kezdeményezése" line. No campaign may fund a party, a party campaign, or a candidate. That boundary is what keeps the platform outside párttörvény (2007. évi CLXXXI. tv.) reporting and donation-cap rules, and it is enforced as an explicit check in the approval queue, not as a policy sentence. Campaign creation is **closed**: creators apply, an admin approves. Main structural departure from the usual reward-crowdfunding model, which is open self-serve. ## 1b. Brand **Name:** Szabadpálya · `szabadpalya.hu` (no DNS as of 2026-08-22 — verify at a registrar and register before any design work ships) **Tagline:** *Szabad a pálya.* ("the track is clear") From the living idiom "szabad a pálya!" — the coast is clear, go ahead, nothing is in your way. Exactly what the platform says to a public project waiting for a permission it never actually needed. `Pálya` also reads as track / field / career: young, forward-moving, unideological. The name carries **szabad** openly. We are not concealing the political lineage — the platform is party-adjacent and that is fine — we are simply not dressing it in Momentum's identity. No Momentum logo, colours, typography or naming conventions. Szabadpálya has its own mark. **Visual basis:** the Momentum **rendszerbontó** brand edition (`../arculat/arculat3_assets/`; the handbook PDF sits in `brand/`, gitignored). | Token | Value | Role | |---|---|---| | Electric purple | `#7360F9` | primary accent, CTA, progress lane | | Lime | `#B9D440` | overshoot, emphasis, the arrow | | Lavender | `#BC9BF3` | secondary chips | | Cream paper | `#FFF1EB` | ground | | Ink | `#241C3B` | text | | Heritage purple | `#8E6FCE` | sparing; ties back to the older identity | Ratio per the handbook: cream + ink ~55%, electric ~25%, lime + lavender ~15%. Typography: **Playfair Display** (display serif) + **Poppins** (body). The handbook has no dark mode — the ink-grounded variant is our extension. **Logo:** placeholder, in `brand/out/`. Three lanes, the middle one open, running into a lime arrow. Text is converted to paths, so it renders without the fonts installed. At 32 px the dimmed lanes wash out — a small-size variant is needed if this mark survives. **Voice** (Hungarian UI copy): second person singular, active verbs, no NGO passive. `Indíts kampányt`, not `Kampány indítása`. `Beleadom`, not `Támogatás`. **Primary CTA vocabulary:** | Context | String | |---|---| | Pledge button (tier) | `Beleadom` | | Main campaign CTA | `Beleadom ebbe a projektbe` | | Launch a campaign | `Indíts kampányt` | | Browse | `Nézd meg, mi épül` | | Hero | `Szabad a pálya.` | ## 1c. Operator A **new egyesület**, founded for this purpose. Not Momentum, not an existing organisation. Consequences to plan around: - The domain, the mark and the Stripe **platform** account are all registered to the egyesület — not to a private person. - Hungarian egyesület registration (törvényszék) takes weeks to months and needs 10 founding members, statutes, and a registered seat. Build can proceed in parallel; **Stripe test mode needs nothing, live mode needs the registered entity**, tax number and bank account. That is the real launch gate, not the code. - Creators onboard as their own Connect accounts, so the egyesület never holds campaign money — it only operates the platform. - With a 0% platform fee the egyesület has running costs (hosting, domain, mail) and no revenue from campaigns. Budget for it, or the "ingyenes" promise is the first thing to break. ## 2. Funding model **Keep-what-you-raise.** Contributions are kept regardless of whether the goal is reached. The progress bar is motivational, not binding. ### Fulfillment guard (mitigation) Physical rewards + immediate charge = shipping debt if the goal misses. Charging is therefore decided **per tier type**: | Tier type | Charge timing | Rationale | |---|---|---| | `donation` (symbolic, no good) | immediate | Nothing owed in return | | `digital` (PDF, credits listing) | immediate | Zero marginal cost to deliver | | `physical` (book, print, bundle) | **on goal reached, else released at close** | Never owe a good you can't produce | If a campaign with physical tiers ends under goal, physical pledges are released uncaptured and the backer is notified; donation and digital money is kept. Config flag `campaign.physical_tier_policy = guard | immediate`. ### Legal consequence Charging for a shippable good is **távollévők közötti szerződés** under 45/2014. (II. 26.) Korm. rendelet — 14-day withdrawal right, ÁFA due at charge, mandatory pre-contract disclosures at checkout. Physical tiers must carry: seller identity, delivery deadline, total price incl. ÁFA and shipping, withdrawal notice. Donation tiers do not. ## 3. Payments and fees **No platform fee. 0%.** `application_fee_amount = 0` on every charge. The operator entity absorbs hosting and running costs; this is cost-recovery-free by design and is a headline selling point against the 4.5–14.9% the commercial platforms charge. **Stripe Connect** (HUF), platform-as-facilitator: - Each approved creator org onboards as a **connected account** (Express). KYC/AML is Stripe's responsibility — important given who stands behind the operator. - Backer pays via **Payment Intent** with `setup_future_usage` so the card is vaulted; guarded physical tiers use **manual capture**, re-authorized at close. - Money never touches the operator's bank account. Creator is merchant of record. - Payout on Stripe's standard schedule, min. net 50 000 Ft threshold. - Phase 2: bank transfer (átutalás) with manual reconciliation for large donors. **Stripe's own processing fee is unavoidable** and is deducted from the creator's payout — we cannot zero that out, only our own cut. The `/dijszabas` page must state this plainly so "ingyenes" is not read as "the creator receives 100%". Verify current Stripe HU card rates before publishing that page; do not hardcode a number in the product from memory. *Open:* offer backers an optional "fedezem a tranzakciós költséget" checkbox at checkout? Common on NGO donation flows, keeps creator payout whole. Not MVP-blocking. ## 4. Page inventory ### 4.1 Campaign detail — `/kampanyok/:slug` 1. **Header** — category chip, title, creator avatar + name, `Kedvencekhez adás` 2. **Media gallery** — carousel, image counter `1/10`, video support 3. **Funding bar** — `Cél: X Ft` · `Összegyűlt: Y Ft` · `N%` progress · `N támogató` · `N nap van hátra` · status badge 4. **Sticky CTA** — `Beleadom ebbe a projektbe`, `Megosztás`, `Megfigyelés` 5. **Tabs** — `Sztori` · `Frissítések` · `Hozzászólások` 6. **Story** — rich content (`Összefoglalás`, `Bemutatás`), `A csapat` bios 7. **Reward tiers** — see 4.2 8. **Footer** Desktop: story left, sticky tier rail right. Mobile: single column, funding bar and CTA pinned to viewport bottom. **No `Támogatók` tab.** The commercial platforms publish a backer list; we do not. Backer identities are never public — not by name, not by avatar, not opt-in. Consequences: - The aggregate **count** stays (`164 támogató`) — non-identifying and load-bearing for social proof. - Full backer list is visible only to the campaign creator in `/vezerlopult`, and only for fulfillment. Never exportable to a general-purpose mailing list. - The `100 000 Ft` reference tier's "name printed in the credits" perk still works — that is the creator's own publication, not platform-published data. Rationale: a donor list attached to a party-adjacent platform is both a GDPR exposure and a political liability. Not collecting public consent at all is cleaner than managing it. ### 4.2 Reward tier card ``` [ -20% ] [ Legnépszerűbb ] 12 000 Ft ~~15 000 Ft~~ Könyv és művészi nyomat · 1 db könyv · 1 db A4 művészi nyomat 14 támogató · 20 db-ra limitálva Várható szállítás: 2026-11-30 [ Beleadom ] ``` Fields: `price`, `list_price?`, `title`, `includes[]`, `backer_count`, `inventory_limit?`, `estimated_delivery`, `is_featured`, `type` (donation | digital | physical), `requires_shipping`. ### 4.3 Other pages | Route | Purpose | |---|---| | `/` | Landing: featured campaigns, how-it-works, social proof | | `/kampanyok` | Browse — filters `Összes` / `Aktív` / `Legnépszerűbb` / by category | | `/kampanyok/:slug/tamogatas` | Checkout: tier → qty → shipping → Stripe → confirm | | `/sikersztorik` | Completed campaigns | | `/hogyan-mukodik` | Explainer | | `/dijszabas` | Zero platform fee + honest note on Stripe processing costs | | `/belepes`, `/regisztracio` | Auth | | `/vezerlopult` | Creator dashboard: editor, backers (private), updates, payouts | | `/admin` | Approval queue, moderation, political-content screening | ## 5. Domain model ``` User ──< Campaign ──< RewardTier ──< Pledge >── User (backer) │ │ ├──< Update └──< ShippingAddress? ├──< Comment └──< Payout ``` **Campaign**: slug, title, category, creator_org, goal_amount, currency=HUF, starts_at, **ends_at**, status (`draft` → `beadva` → `aktiv` → `finanszirozott` / `lezart` / `elutasitva`), story (rich), media[], team[], physical_tier_policy. **Pledge**: backer, tier, quantity, amount, stripe_payment_intent_id, status (`authorized` | `captured` | `released` | `refunded`), shipping_address?. No `is_anonymous` field — every pledge is private, so there is nothing to toggle. **Derived, never stored:** raised_amount, backer_count, progress_pct, days_left. Computed from captured + authorized pledges so the bar cannot drift from reality. ## 6. Campaign duration The creator picks a concrete **end date**, not a length in days. It is shown as a date everywhere in the creator UI (`A kampány eddig tart: 2026-11-30`) and rendered as a countdown only on the public page (`9 nap van hátra`). - Bounded: `ends_at` must fall within **90 days** of launch. Configurable ceiling, enforced server-side at submit and at launch approval. - Campaigns close at **23:59:59 Europe/Budapest** on the chosen date, not at the wall-clock instant of launch — a Hungarian creator picking "november 30" means the whole day. - Extension after launch: admin-only, and never past the 90-day ceiling. Creators cannot quietly move their own deadline once money is in. - `estimated_delivery` on physical tiers must be **after** `ends_at`. Validated. ## 7. Key flows **Back a project** — tier select → qty → shipping (if `requires_shipping`) → account or guest email → Stripe Payment Element → confirm → receipt email. No public trace. **Campaign close** — scheduled job at `ends_at`: evaluate goal → capture guarded physical pledges or release them → notify backers → notify creator → open the fulfillment view in `/vezerlopult`. **Creator apply** — form → admin review (incl. explicit political-content check) → Stripe Connect onboarding → editor unlocked → submit for launch approval. ## 8. Stack Self-hosted on our own managed server. Everything below runs on one box under Docker Compose; nothing depends on a PaaS. **Two languages, on purpose.** The stack was TypeScript end to end until the backend was rewritten in Go; the original argument (one language, one dependency tree) was sound and is now overridden by a deliberate decision. What it costs is recorded honestly below, because the reasoning that produced this table should not have to be reconstructed from a diff later. ### Application | Layer | Choice | Why | |---|---|---| | Backend | **Go + Gin**, single static binary | One process serves the API, the HTML document, the share images and the built assets. 49 MB image, nothing to install. | | Frontend | **React 19 + React Router + Vite** | The campaign pages were already components; only their data source changed. | | HTML document | **Go `html/template`** | Per-campaign `og:` tags without a JS runtime — see below. | | DB | **Postgres 17**, same host | Tiny dataset. No managed service needed at this scale. | | Data access | **pgx + hand-written SQL** | The three queries that matter are aggregates over pledges; SQL states them more plainly than an ORM did. Migrations are plain `.sql` applied by a ~60-line runner. | | Auth | **Own magic links** (`api/internal/auth`) | Go has no better-auth. ~200 lines: hashed tokens, single-use redemption, cookie sessions. | | Share images | **Drawn in Go** (`api/internal/ogimage`) | Go has no Satori. Rectangles and text on a 1200×630 canvas. | | Editor | **Tiptap** | Campaign story rich text. Not built yet. | | UI | **Tailwind 4**, rendszerbontó tokens | See 1b. | | Proxy/TLS | **Caddy** | Automatic Let's Encrypt, two lines of config. | | Deploy | **Docker Compose** on the box | `db`, `migrate`, `app`. Nothing depends on a PaaS. | **What the second language costs.** Money code loses Stripe's TypeScript types, which this document previously called the best guardrail available for it — Go's Stripe library is typed but less expressive about the shapes Stripe actually returns. Payments are still unbuilt (section 2 of TODO.md), so that cost lands in full on the work that has not started yet. Nothing about the rewrite makes it easier; it is a price paid for the rest of the stack. **Why the server renders HTML:** the commercial Hungarian platforms are client-rendered SPAs — fetching a campaign page returns a ~3 KB empty shell, and the real content only exists after JavaScript runs. Every campaign shared to Facebook therefore shows the same generic site-wide card instead of the project's own photo, title and progress. For a platform whose growth *is* sharing, that is a permanent self-inflicted wound and the cheapest place for us to be strictly better. Go renders the document, React renders the page. The seam is sharp: Go owns `
` — title, description, `og:` tags, the theme script, and the hashed asset tags read out of Vite's `manifest.json` — and React owns everything inside `#root`. Nothing is rendered in two places, so nothing can drift. **What this does and does not buy, stated plainly.** It buys the share cards, which is the harm named above: crawlers read `og:` tags without executing JavaScript. It does *not* buy what full SSR bought — the campaign page no longer ships ~56 KB of real HTML, and a reader with JavaScript off sees nothing. Most of the practical difference is clawed back by inlining the data the server already loaded to build the tags, in a `