Phase 12 R0+R1: Marketplace-Restore-Plan + Schema in marketplace-pgSchema

R0 (Doku):
- Archiv unter docs/marketplace/archive/ aus managarten-Tag
  cards-decommission-base: MARKETPLACE_PLAN (654 Z., Vollvision mit
  mana-credits-Flow, Anti-Patterns), COMPETITORS, GUIDELINES,
  cards-server_CLAUDE.
- docs/playbooks/MARKETPLACE_RESTORE.md mit Schema-Naming-Entscheidung
  (eigenes marketplace-pgSchema), Wellen R0-R6, Cardecky-Skill-
  Integration, Lizenz-Modell.
- CLAUDE.md Invariante 2: Strategie-B gilt nur für Study-/FSRS-/Sync-
  Schicht; Marketplace-Restore ist explizite Ausnahme.
- STATUS.md: Phase 12 R0+R1 durch.

R1 (Schema):
- 16 Tabellen + 5 Enums im neuen marketplace-pgSchema (authors,
  decks, deck_versions, deck_cards, tag_definitions, deck_tags,
  deck_stars, deck_subscriptions, deck_forks, deck_pull_requests,
  card_discussions, deck_reports, ai_moderation_log, deck_purchases,
  author_payouts, author_follows).
- drizzle.config.ts: schemaFilter ['cards', 'marketplace'].
- Greenfield cards-pgSchema unangetastet.
- DB-CHECK decks_price_requires_license verifiziert (paid Deck mit
  CC-BY wirft sauber ab).
- type-check + 56 API-Tests grün, drizzle-kit push idempotent.

Decks dormant (kein Code-Pfad ruft die Tabellen). R2 (Backend α/β:
Author-Profile + Publish + AI-Mod) als nächstes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-09 15:05:22 +02:00
parent e596199ba0
commit 9a7068dd19
17 changed files with 2404 additions and 4 deletions

View file

@ -54,8 +54,16 @@ Diese sind beschlossen. Nicht ohne explizite Diskussion antasten:
1. **Server-authoritative MVP.** Keine Dexie, keine IndexedDB, keine 1. **Server-authoritative MVP.** Keine Dexie, keine IndexedDB, keine
eigene Sync-Engine. Frontend = HTTP-Client zu cards-api. Local-First eigene Sync-Engine. Frontend = HTTP-Client zu cards-api. Local-First
später via mana-sync-Federation, nicht durch eigenen Stack. später via mana-sync-Federation, nicht durch eigenen Stack.
2. **Kein Code aus mana-monorepo kopiert.** Code dort wird gelesen 2. **Kein Code aus mana-monorepo kopiert — für die Study-/FSRS-/Sync-
(Lessons-Doc), nicht übernommen. Sauber neu ab Tag 0. Schicht.** Diese Architektur (Server-authoritative, kein Dexie, neue
Type-Hierarchie) ist sauber neu ab Tag 0. **Ausnahme: Marketplace-
Restore.** Der ehemalige `services/cards-server/` aus mana-monorepo
war nie auf der Strategie-B-Verbots-Liste — er wurde am 2026-05-08
nur mit-rausgerissen, weil er an `apps/cards/` gekoppelt war. Bei
einem Restore wird der Marketplace-Code aus dem
`cards-decommission-base`-Tag im managarten-Repo additiv re-import'd,
in eigenes `marketplace`-pgSchema, additiv zur Study-Welt. Plan:
[`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md).
3. **Eigene Postgres-DB `cards`** im geteilten Mana-Cluster, Schema- 3. **Eigene Postgres-DB `cards`** im geteilten Mana-Cluster, Schema-
Isolation via `pgSchema('cards')`. Isolation via `pgSchema('cards')`.
4. **Föderation über `@mana/shared-app-tpl`.** Pflicht-Endpoints 4. **Föderation über `@mana/shared-app-tpl`.** Pflicht-Endpoints

View file

@ -98,6 +98,7 @@ Vollständiger Plan: [`mana/docs/playbooks/CARDS_GREENFIELD.md`](../mana/docs/pl
| 9 | Polish (DSGVO-UI, Settings, Account, Statistik, i18n, A11y, Media, Image-Occlusion) | 🟡 weit | Card-Edit + Cloze-Editor + Inbox-Banner + Account/DSGVO + Statistik + Pre-Flight-Swap + i18n DE/EN + A11y-Pass + Cloze-Hint-Anzeige + Anki-Re-Import-Dedupe + MinIO-Media-Upload + Image-Occlusion durch (9a9l). Verbleibend: type-in, audio, multiple-choice (Schema vorbereitet) | | 9 | Polish (DSGVO-UI, Settings, Account, Statistik, i18n, A11y, Media, Image-Occlusion) | 🟡 weit | Card-Edit + Cloze-Editor + Inbox-Banner + Account/DSGVO + Statistik + Pre-Flight-Swap + i18n DE/EN + A11y-Pass + Cloze-Hint-Anzeige + Anki-Re-Import-Dedupe + MinIO-Media-Upload + Image-Occlusion durch (9a9l). Verbleibend: type-in, audio, multiple-choice (Schema vorbereitet) |
| 10 | Production-Deploy (Mac Mini, Cloudflare-Tunnel) | ✅ live 2026-05-08 | cardecky.mana.how + cardecky-api.mana.how, alte cards.* via nginx-301-Redirect | | 10 | Production-Deploy (Mac Mini, Cloudflare-Tunnel) | ✅ live 2026-05-08 | cardecky.mana.how + cardecky-api.mana.how, alte cards.* via nginx-301-Redirect |
| 11 | Decommission Cards-Modul aus mana-monorepo | ✅ 2026-05-08 | apps/cards, services/cards-server, packages/cards-core, mana-app cards-Modul + cross-refs entfernt (4 Commits, type-check 0 errors) | | 11 | Decommission Cards-Modul aus mana-monorepo | ✅ 2026-05-08 | apps/cards, services/cards-server, packages/cards-core, mana-app cards-Modul + cross-refs entfernt (4 Commits, type-check 0 errors) |
| 12 | Marketplace-Restore (R0R6) | 🟡 R0+R1 durch | Plan: [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). R0 (Doku-Archiv aus `cards-decommission-base` + Restore-Plan + Strategie-B-Klarstellung in CLAUDE.md): ✅. R1 (16 Tabellen + 5 Enums in `marketplace`-pgSchema, drizzle-kit push grün, type-check + 56 Tests grün, CHECK-Constraint `decks_price_requires_license` verifiziert): ✅. Verbleibend: R2 Backend α/β (Author-Profile + Publish + AI-Mod), R3 γ/δ (Discovery + Subscribe + Smart-Merge), R4 ε (PRs + Discussions), R5 Frontend-Routes, R6 E2E-Smoke. |
Legende: ✅ erledigt + verifiziert · 🚧 blockiert · ⏸ noch nicht begonnen Legende: ✅ erledigt + verifiziert · 🚧 blockiert · ⏸ noch nicht begonnen

View file

@ -1,13 +1,13 @@
import type { Config } from 'drizzle-kit'; import type { Config } from 'drizzle-kit';
export default { export default {
schema: './src/db/schema/*.ts', schema: ['./src/db/schema/*.ts', './src/db/schema/marketplace/*.ts'],
out: './src/db/migrations', out: './src/db/migrations',
dialect: 'postgresql', dialect: 'postgresql',
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL ?? 'postgresql://cards:cards@localhost:5435/cards', url: process.env.DATABASE_URL ?? 'postgresql://cards:cards@localhost:5435/cards',
}, },
schemaFilter: ['cards'], schemaFilter: ['cards', 'marketplace'],
verbose: true, verbose: true,
strict: true, strict: true,
} satisfies Config; } satisfies Config;

View file

@ -0,0 +1,23 @@
import { pgSchema } from 'drizzle-orm/pg-core';
/**
* Public-Marketplace-Tabellen leben unter einem eigenen
* `marketplace`-pgSchema in derselben `cards`-DB. Bewusste Trennung
* zur privaten Lern-Welt (`cards`-pgSchema):
*
* - sauberer Read-Path: Public-Endpoints sehen nur `marketplace`,
* Greenfield-Code nur `cards`.
* - Backup-Granularität (`pg_dump --schema=marketplace`).
* - RLS-Policies pro Schema möglich (Take-Down-Workflows).
*
* Drizzle-Variablennamen tragen bewusst den `public`-Prefix
* (`publicDecks`, `publicDeckVersions`, ) um in Imports klar
* vom privaten `cards.decks` getrennt zu sein.
*
* Geschichte: 1:1 portiert aus dem alten
* `mana-monorepo/services/cards-server/src/db/schema/_schema.ts`
* (managarten-Tag `cards-decommission-base`, 2026-05-08), nur das
* Schema-Target gewechselt von `cards` auf `marketplace`. Plan:
* `docs/playbooks/MARKETPLACE_RESTORE.md`.
*/
export const marketplaceSchema = pgSchema('marketplace');

View file

@ -0,0 +1,68 @@
/**
* Authors Public-Identität für User, die Decks publishen.
*
* Eine Author-Row pro User, der jemals ein Author-Profil angelegt hat.
* `userId` ist plain-text-Referenz auf `auth.users.id` (cross-DB
* keine FK auf DB-Ebene; das konsumierende Service validiert JWTs
* von mana-auth und nimmt den `sub`-Claim verbatim).
*
* Verifizierung hat zwei orthogonale Achsen:
* - `verified_mana`: manuell vom Mana-Verein vergeben (Lehrer,
* Pädagog:innen, Ärzt:innen). Nicht erkaufbar.
* - `verified_community`: automatisch berechnet aus Engagement
* ( X stars, Y featured Decks, Z Subscribers). Periodisch
* re-evaluiert.
*
* Beide Achsen können gleichzeitig true sein UI zeigt beide Badges.
*/
import { boolean, index, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { marketplaceSchema } from './_schema.ts';
export const authors = marketplaceSchema.table(
'authors',
{
userId: text('user_id').primaryKey(),
slug: text('slug').notNull(),
displayName: text('display_name').notNull(),
bio: text('bio'),
avatarUrl: text('avatar_url'),
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull(),
// Pseudonym-Modus: Klarname versteckt, nur displayName sichtbar.
pseudonym: boolean('pseudonym').default(false).notNull(),
// Verifizierungs-Flags (siehe Header).
verifiedMana: boolean('verified_mana').default(false).notNull(),
verifiedCommunity: boolean('verified_community').default(false).notNull(),
// Soft-Ban: gebannter Author kann nicht mehr publishen, existierende
// Decks bleiben lesbar mit „deactivated"-Badge.
bannedAt: timestamp('banned_at', { withTimezone: true }),
bannedReason: text('banned_reason'),
},
(t) => ({
slugIdx: uniqueIndex('authors_slug_idx').on(t.slug),
verifiedIdx: index('authors_verified_idx').on(t.verifiedMana, t.verifiedCommunity),
})
);
/**
* Following-Beziehung User Author. Treibt den persönlichen Activity-Feed.
*/
export const authorFollows = marketplaceSchema.table(
'author_follows',
{
followerUserId: text('follower_user_id').notNull(),
authorUserId: text('author_user_id')
.notNull()
.references(() => authors.userId, { onDelete: 'cascade' }),
since: timestamp('since', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
pk: uniqueIndex('author_follows_pk').on(t.followerUserId, t.authorUserId),
authorIdx: index('author_follows_author_idx').on(t.authorUserId),
followerIdx: index('author_follows_follower_idx').on(t.followerUserId),
})
);
export type AuthorRow = typeof authors.$inferSelect;
export type AuthorInsert = typeof authors.$inferInsert;

View file

@ -0,0 +1,68 @@
/**
* Two-sided Marketplace-Bookkeeping. Das eigentliche Geld lebt in
* mana-credits wir tracken nur das Deck-Purchase-Event und den
* abgeleiteten Author-Payout, damit Buyer-History, Author-Dashboards
* und Reconcile gegen den mana-credits-Ledger möglich sind.
*
* **Status R1 (Schema-Restore):** Tabellen kommen mit, aber Code-Pfade
* (Routes/Services) bleiben dormant bis nach Live-Validation der
* gratis-Decks. Re-Aktivierung als eigene Welle.
*/
import { index, integer, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
import { marketplaceSchema } from './_schema.ts';
import { authors } from './authors.ts';
import { publicDecks, publicDeckVersions } from './decks.ts';
export const deckPurchases = marketplaceSchema.table(
'deck_purchases',
{
id: uuid('id').primaryKey().defaultRandom(),
buyerUserId: text('buyer_user_id').notNull(),
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'restrict' }),
// Snapshot der Version zum Kaufzeitpunkt — Buyer behält Lifetime-
// Access auf alle nachfolgenden Versionen.
versionId: uuid('version_id')
.notNull()
.references(() => publicDeckVersions.id, { onDelete: 'restrict' }),
// Snapshot des Preises zum Kaufzeitpunkt.
priceCredits: integer('price_credits').notNull(),
// Pre-computed Split (Sum = priceCredits modulo Rundung).
authorShare: integer('author_share').notNull(),
manaShare: integer('mana_share').notNull(),
// Reference in mana-credits-Ledger.
creditsTransaction: text('credits_transaction'),
purchasedAt: timestamp('purchased_at', { withTimezone: true }).defaultNow().notNull(),
refundedAt: timestamp('refunded_at', { withTimezone: true }),
},
(t) => ({
// Ein Kauf pro Buyer pro Deck — covered Lifetime-Access.
uniqueBuyerDeck: uniqueIndex('deck_purchases_buyer_deck_idx').on(t.buyerUserId, t.deckId),
buyerIdx: index('deck_purchases_buyer_idx').on(t.buyerUserId),
deckIdx: index('deck_purchases_deck_idx').on(t.deckId),
})
);
export const authorPayouts = marketplaceSchema.table(
'author_payouts',
{
id: uuid('id').primaryKey().defaultRandom(),
authorUserId: text('author_user_id')
.notNull()
.references(() => authors.userId, { onDelete: 'restrict' }),
sourcePurchaseId: uuid('source_purchase_id')
.notNull()
.references(() => deckPurchases.id, { onDelete: 'restrict' }),
creditsGranted: integer('credits_granted').notNull(),
// Reference in mana-credits-Grant-Ledger.
creditsGrantId: text('credits_grant_id'),
grantedAt: timestamp('granted_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
authorIdx: index('author_payouts_author_idx').on(t.authorUserId),
purchaseIdx: index('author_payouts_purchase_idx').on(t.sourcePurchaseId),
})
);

View file

@ -0,0 +1,138 @@
/**
* Public-Decks, Versionen, Karten.
*
* Ein Deck ist das langlebige Identitäts-Objekt (Spanish A2 Vocab"),
* zeigt immer auf eine `latest_version_id`. Versionen sind immutable
* Snapshots einmal published, ändern sie sich nie. Karten sind
* Versions-skopiert und tragen einen per-Karten-Content-Hash, damit
* Subscriber-Smart-Merge den FSRS-State unveränderter Karten über
* Versions-Bumps hinweg erhalten kann.
*
* `price_credits = 0` heißt kostenlos. Alles > 0 erzwingt die
* Cardecky-Pro-Only-1.0-Lizenz (CHECK constraint auf DB-Ebene).
*/
import { sql } from 'drizzle-orm';
import {
boolean,
check,
index,
integer,
jsonb,
text,
timestamp,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import { marketplaceSchema } from './_schema.ts';
import { authors } from './authors.ts';
/**
* Spiegelt `CardType` aus `@cards/domain`. Enum lebt im
* `marketplace`-pgSchema (nicht im default `public`), damit
* drizzle-kit-Push mit `schemaFilter: ['cards', 'marketplace']`
* keine spurious CREATE-TYPE-Versuche macht.
*/
export const cardTypeEnum = marketplaceSchema.enum('card_type', [
'basic',
'basic-reverse',
'cloze',
'type-in',
'image-occlusion',
'audio',
'multiple-choice',
]);
export const publicDecks = marketplaceSchema.table(
'decks',
{
id: uuid('id').primaryKey().defaultRandom(),
slug: text('slug').notNull(),
title: text('title').notNull(),
description: text('description'),
// ISO-639-1 (z.B. 'de', 'en', 'es'). Nullable für mixed-language.
language: text('language'),
// SPDX-style ID. CC0-1.0, CC-BY-4.0, CC-BY-SA-4.0,
// Cardecky-Personal-Use-1.0 (default für free), Cardecky-Pro-Only-1.0 (paid).
license: text('license').notNull().default('Cardecky-Personal-Use-1.0'),
priceCredits: integer('price_credits').notNull().default(0),
ownerUserId: text('owner_user_id')
.notNull()
.references(() => authors.userId, { onDelete: 'restrict' }),
// Updated bei jedem Publish einer neuen Version.
latestVersionId: uuid('latest_version_id'),
isFeatured: boolean('is_featured').notNull().default(false),
isTakedown: boolean('is_takedown').notNull().default(false),
takedownAt: timestamp('takedown_at', { withTimezone: true }),
takedownReason: text('takedown_reason'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
slugIdx: uniqueIndex('decks_slug_idx').on(t.slug),
ownerIdx: index('decks_owner_idx').on(t.ownerUserId),
featuredIdx: index('decks_featured_idx').on(t.isFeatured),
// Paid Decks müssen die Pro-Only-Lizenz tragen.
priceLicense: check(
'decks_price_requires_license',
sql`price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0'`
),
})
);
export const publicDeckVersions = marketplaceSchema.table(
'deck_versions',
{
id: uuid('id').primaryKey().defaultRandom(),
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
// SemVer string — ordering passiert in app code, nicht in der DB.
semver: text('semver').notNull(),
changelog: text('changelog'),
// SHA-256 über die canonicalized Karten-Liste — Clients erkennen
// damit „hat sich was geändert" ohne Payload-Diff.
contentHash: text('content_hash').notNull(),
cardCount: integer('card_count').notNull(),
publishedAt: timestamp('published_at', { withTimezone: true }).defaultNow().notNull(),
// Ältere Versionen bleiben lesbar, neue Subscriber gehen auf latest.
deprecatedAt: timestamp('deprecated_at', { withTimezone: true }),
},
(t) => ({
uniqueSemver: uniqueIndex('deck_versions_deck_semver_idx').on(t.deckId, t.semver),
deckIdx: index('deck_versions_deck_idx').on(t.deckId),
hashIdx: index('deck_versions_hash_idx').on(t.contentHash),
})
);
export const publicDeckCards = marketplaceSchema.table(
'deck_cards',
{
id: uuid('id').primaryKey().defaultRandom(),
versionId: uuid('version_id')
.notNull()
.references(() => publicDeckVersions.id, { onDelete: 'cascade' }),
// Spiegelt @cards/domain CardType.
type: cardTypeEnum('type').notNull(),
// Free-form key/value bag.
// basic / basic-reverse / type-in: { front, back }
// cloze: { text, extra? }
fields: jsonb('fields').$type<Record<string, string>>().notNull(),
ord: integer('ord').notNull(),
// SHA-256 über canonical(type, fields). Subscriber nutzen den
// Hash für per-Karten-Smart-Merge — unveränderte Karten behalten
// ihren FSRS-State über Versions-Pulls hinweg. **Identisch zur
// Berechnung in `@cards/domain` `cardContentHash`** — das ist der
// SoT, dieser Hash ist nur die persistierte Form.
contentHash: text('content_hash').notNull(),
},
(t) => ({
uniqueOrd: uniqueIndex('deck_cards_version_ord_idx').on(t.versionId, t.ord),
hashIdx: index('deck_cards_hash_idx').on(t.contentHash),
})
);
export type PublicDeckRow = typeof publicDecks.$inferSelect;
export type PublicDeckInsert = typeof publicDecks.$inferInsert;
export type PublicDeckVersionRow = typeof publicDeckVersions.$inferSelect;
export type PublicDeckCardRow = typeof publicDeckCards.$inferSelect;

View file

@ -0,0 +1,81 @@
/**
* Pull-Requests + Card-Discussions.
*
* Pull-Requests propose card-level Änderungen am Deck; der Author kann
* mergen cards-server erstellt eine neue Version automatisch. Der
* Diff wird als JSON-Blob (`{ add, modify, remove }`) gespeichert,
* damit eine GitHub-style Review-UI gerendert werden kann ohne aus
* Versions-Diffs neu abzuleiten.
*
* Card-Discussions sind an `card_content_hash` gebunden (nicht an
* `card_id`), damit Threads Versions-Bumps überleben solange die
* Karte selbst unverändert bleibt.
*/
import { boolean, index, jsonb, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { marketplaceSchema } from './_schema.ts';
import { publicDecks, publicDeckVersions } from './decks.ts';
export const pullRequestStatusEnum = marketplaceSchema.enum('pr_status', [
'open',
'merged',
'closed',
'rejected',
]);
export interface PullRequestDiff {
add: { type: string; fields: Record<string, string> }[];
modify: { contentHash: string; fields: Record<string, string> }[];
remove: { contentHash: string }[];
}
export const deckPullRequests = marketplaceSchema.table(
'deck_pull_requests',
{
id: uuid('id').primaryKey().defaultRandom(),
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
authorUserId: text('author_user_id').notNull(),
status: pullRequestStatusEnum('status').notNull().default('open'),
title: text('title').notNull(),
body: text('body'),
diff: jsonb('diff').$type<PullRequestDiff>().notNull(),
mergedIntoVersionId: uuid('merged_into_version_id').references(() => publicDeckVersions.id, {
onDelete: 'set null',
}),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
resolvedAt: timestamp('resolved_at', { withTimezone: true }),
},
(t) => ({
deckIdx: index('deck_pull_requests_deck_idx').on(t.deckId),
statusIdx: index('deck_pull_requests_status_idx').on(t.deckId, t.status),
authorIdx: index('deck_pull_requests_author_idx').on(t.authorUserId),
})
);
export const cardDiscussions = marketplaceSchema.table(
'card_discussions',
{
id: uuid('id').primaryKey().defaultRandom(),
// Bound to card content_hash, not row id, so der Thread folgt der
// Karte über Versions-Bumps solange Inhalt bleibt.
cardContentHash: text('card_content_hash').notNull(),
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
authorUserId: text('author_user_id').notNull(),
// Threading: parent_id NULL = root post, NOT NULL = reply.
parentId: uuid('parent_id'),
body: text('body').notNull(),
// Hidden by Author or Mod. Nicht gelöscht — Audit-Trail erhalten.
hidden: boolean('hidden').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
hashIdx: index('card_discussions_hash_idx').on(t.cardContentHash),
deckIdx: index('card_discussions_deck_idx').on(t.deckId),
parentIdx: index('card_discussions_parent_idx').on(t.parentId),
})
);

View file

@ -0,0 +1,67 @@
/**
* Engagement-Primitives Stars (Bookmarks), Subscriptions (Live-
* Updates), Forks (eigene Kopie mit Lineage).
*
* Alle keyed auf `user_id` text plain reference auf auth.users.id
* ohne cross-DB-FK.
*/
import { boolean, index, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
import { marketplaceSchema } from './_schema.ts';
import { publicDecks, publicDeckVersions } from './decks.ts';
export const deckStars = marketplaceSchema.table(
'deck_stars',
{
userId: text('user_id').notNull(),
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
starredAt: timestamp('starred_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
pk: uniqueIndex('deck_stars_pk').on(t.userId, t.deckId),
deckIdx: index('deck_stars_deck_idx').on(t.deckId),
})
);
export const deckSubscriptions = marketplaceSchema.table(
'deck_subscriptions',
{
userId: text('user_id').notNull(),
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
// Letzte Version, die der User gepullt hat. Smart-Merge vergleicht
// das mit `deck.latest_version_id` für den Diff.
currentVersionId: uuid('current_version_id').references(() => publicDeckVersions.id, {
onDelete: 'set null',
}),
subscribedAt: timestamp('subscribed_at', { withTimezone: true }).defaultNow().notNull(),
notifyUpdates: boolean('notify_updates').notNull().default(true),
},
(t) => ({
pk: uniqueIndex('deck_subscriptions_pk').on(t.userId, t.deckId),
deckIdx: index('deck_subscriptions_deck_idx').on(t.deckId),
userIdx: index('deck_subscriptions_user_idx').on(t.userId),
})
);
export const deckForks = marketplaceSchema.table(
'deck_forks',
{
userId: text('user_id').notNull(),
sourceDeckId: uuid('source_deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
sourceVersionId: uuid('source_version_id')
.notNull()
.references(() => publicDeckVersions.id, { onDelete: 'cascade' }),
forkedAt: timestamp('forked_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
pk: uniqueIndex('deck_forks_pk').on(t.userId, t.sourceDeckId, t.sourceVersionId),
sourceIdx: index('deck_forks_source_idx').on(t.sourceDeckId),
})
);

View file

@ -0,0 +1,19 @@
/**
* Re-Exports für das gesamte Marketplace-Schema. Hält Imports flach
* downstream code macht
* `import { authors, publicDecks } from '../db/schema/marketplace'`.
*
* **Naming-Konvention:** Variablennamen tragen `public`-Prefix
* (`publicDecks`, `publicDeckVersions`, `publicDeckCards`), damit
* Imports zusammen mit den Greenfield-`cards.decks`/`cards.cards` aus
* `../schema/index.ts` nicht kollidieren.
*/
export { marketplaceSchema } from './_schema.ts';
export * from './authors.ts';
export * from './decks.ts';
export * from './tags.ts';
export * from './engagement.ts';
export * from './discussions.ts';
export * from './moderation.ts';
export * from './credits.ts';

View file

@ -0,0 +1,83 @@
/**
* Moderation User-Reports + AI-First-Pass-Log.
*
* Reports fließen in eine Mana-Admin-Inbox; AI-Mod-Log ist ein Record
* jeder automatisierten Prüfung pro Version, damit wir auditieren /
* re-train können wenn ein bad outcome shipped.
*/
import { boolean, index, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { marketplaceSchema } from './_schema.ts';
import { publicDecks, publicDeckVersions } from './decks.ts';
export const reportCategoryEnum = marketplaceSchema.enum('report_category', [
'spam',
'copyright',
'nsfw',
'misinformation',
'hate',
'other',
]);
export const reportStatusEnum = marketplaceSchema.enum('report_status', [
'open',
'dismissed',
'actioned',
]);
export const deckReports = marketplaceSchema.table(
'deck_reports',
{
id: uuid('id').primaryKey().defaultRandom(),
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
versionId: uuid('version_id').references(() => publicDeckVersions.id, {
onDelete: 'set null',
}),
// Optional: Report scoped auf eine spezifische Karte by content-hash.
cardContentHash: text('card_content_hash'),
reporterUserId: text('reporter_user_id').notNull(),
category: reportCategoryEnum('category').notNull(),
body: text('body'),
status: reportStatusEnum('status').notNull().default('open'),
resolvedBy: text('resolved_by'),
resolvedAt: timestamp('resolved_at', { withTimezone: true }),
resolutionNotes: text('resolution_notes'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
deckIdx: index('deck_reports_deck_idx').on(t.deckId),
statusIdx: index('deck_reports_status_idx').on(t.status),
})
);
export const aiModerationVerdictEnum = marketplaceSchema.enum('ai_mod_verdict', [
'pass',
'flag',
'block',
]);
export const aiModerationLog = marketplaceSchema.table(
'ai_moderation_log',
{
id: uuid('id').primaryKey().defaultRandom(),
versionId: uuid('version_id')
.notNull()
.references(() => publicDeckVersions.id, { onDelete: 'cascade' }),
verdict: aiModerationVerdictEnum('verdict').notNull(),
// Categories die das Modell flagged — array weil ein Verdict
// mehrere Kategorien hitten kann (z.B. „spam" + „misinformation").
categories: text('categories').array(),
model: text('model'),
rationale: text('rationale'),
humanReviewed: boolean('human_reviewed').notNull().default(false),
humanOverrode: boolean('human_overrode').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
versionIdx: index('ai_moderation_log_version_idx').on(t.versionId),
verdictIdx: index('ai_moderation_log_verdict_idx').on(t.verdict),
})
);

View file

@ -0,0 +1,48 @@
/**
* Tag-Taxonomie für den Marketplace. Hierarchisch (parent_id), kuratiert
* vom Mana-Verein für den kanonischen Baum (medizin > anatomie > kardio).
* Authoren picken aus existierenden Tags und können neue über einen
* moderierten Flow vorschlagen (`curated = false` Admin reviewed).
*
* Bewusst getrennt von der bestehenden `cards.tags`-Tabelle (privat,
* Deck-skopiert) anderer Use-Case (Discovery vs. Lokal-Organisation),
* andere Lebenszyklen.
*/
import { boolean, index, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
import { marketplaceSchema } from './_schema.ts';
import { publicDecks } from './decks.ts';
export const tagDefinitions = marketplaceSchema.table(
'tag_definitions',
{
id: uuid('id').primaryKey().defaultRandom(),
slug: text('slug').notNull(),
name: text('name').notNull(),
parentId: uuid('parent_id'),
description: text('description'),
curated: boolean('curated').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
},
(t) => ({
slugIdx: uniqueIndex('tag_definitions_slug_idx').on(t.slug),
parentIdx: index('tag_definitions_parent_idx').on(t.parentId),
})
);
export const deckTags = marketplaceSchema.table(
'deck_tags',
{
deckId: uuid('deck_id')
.notNull()
.references(() => publicDecks.id, { onDelete: 'cascade' }),
tagId: uuid('tag_id')
.notNull()
.references(() => tagDefinitions.id, { onDelete: 'cascade' }),
},
(t) => ({
pk: uniqueIndex('deck_tags_pk').on(t.deckId, t.tagId),
tagIdx: index('deck_tags_tag_idx').on(t.tagId),
})
);

View file

@ -0,0 +1,353 @@
# Cardecky — Konkurrenz-Analyse (Mai 2026)
> Stand: 2026-05-07. Quellen primär aus offiziellen Pricing-Seiten, G2/Trustpilot/Reddit/HN sowie Wikipedia/Crunchbase. Wo Daten fehlen oder nicht öffentlich sind, ist das explizit vermerkt. Preise schwanken regional/saisonal — die hier genannten Zahlen sind Listenpreise USD, sofern nicht anders angegeben.
---
## 1. Executive Summary
- **Anki bleibt der unschlagbare technische Gold-Standard**, aber UX-Schwächen (FSRS-„Difficulty Hell", Plugin-Hölle, kein natives Cloud-Sync mit Bildern) und der $25 iOS-Preis sind reale Lücken, in die wir stoßen können. Die Übergabe an AnkiHub im Februar 2026 könnte mittelfristig die Open-Source-Dynamik verändern — Beobachten lohnt.
- **Quizlet hat seine eigene Userbase verärgert**: Trustpilot 1.4/5, massive Beschwerden über Paywalls für Funktionen, die früher gratis waren. Genau dieses Vertrauensvakuum füllen Knowt und potenziell wir.
- **AI-Karten-Generierung ist Tischeinsatz, kein Differenzierer mehr.** Quizlet, Quizgecko, Knowt, RemNote, Wisdolia, sogar Memrise haben es. PDF-Import + KI ist erwartete Baseline.
- **Die „beautiful Anki"-Lücke ist umkämpft**: Mochi (5$/mo), RemNote (8$/mo), Noji (vormals AnkiPro). Cardecky mit _kostenlosem_ Sync sticht heraus — niemand sonst bietet die Kombination Markdown + FSRS + Cloud-Sync gratis. Das ist unsere wichtigste objektive Differenzierung.
- **Brand-Sniping ist real und schädlich**: AnkiPro (jetzt Noji) und AnkiApp (jetzt AlgoApp) haben sich einen Ruf als „Anki-Klone, die täuschen" erarbeitet — inkl. eines 10-tägigen Sync-Outages bei AnkiPro im Mai 2025. Lehre für uns: nie Anki im Namen führen, Kompatibilität sauber kommunizieren.
---
## 2. Vergleichstabelle
| Konkurrent | USP-Kurz | Lizenz | Free-Tier | Pro-Preis | Bedrohung |
| ------------------------------ | -------------------------------------- | --------------------------- | -------------------------------- | ----------------------------------------------- | -------------------------------- |
| **Anki (Desktop/Web/Android)** | Tech-Gold-Standard, FSRS, Add-ons | AGPL-3.0 | Voll-Funktional gratis | $0 (iOS: $24.99-29.99 lifetime) | **Hoch** |
| **AnkiHub** | Kollaborative Anki-Decks (USMLE-Fokus) | proprietär (auf Anki-Basis) | Trial | $5/mo | Mittel (Power-User) |
| **Quizlet** | Marktführer Volumen + Schule | proprietär | Sehr eingeschränkt, viele Ads | $35.99/Jahr (Plus), ~$45/Jahr (Unlimited) | **Hoch** (Reichweite) |
| **RemNote** | Notes + SR Hybrid | proprietär | Großzügig (3 PDFs, 5 Image-Occ.) | $8/mo annual (Pro) | Mittel |
| **Mochi** | Markdown, Local-First, schickes UI | proprietär | Single-Device | $5/mo (Sync) | **Hoch** (direkter Wettbewerber) |
| **Brainscape** | Confidence-Based-Repetition | proprietär | Limited Decks | ~$19.99/mo, $79.99 lifetime | Gering-Mittel |
| **Memrise** | Sprachen + AI-Buddies | proprietär | Eingeschränkt | $130.99/Jahr, $199.99 lifetime | Gering (Nische Sprachen) |
| **SuperMemo** | Algorithmus-Urvater (SM-20) | proprietär | Monatstrial Mobile | ~9.90$/mo Mobile, ~$66 Desktop perp. | Gering (Nische, sperrige UX) |
| **AnkiPro / Noji** | „Anki-Look" mit modernem UI | proprietär | mit Ads/Limits | nicht öffentlich klar (~$5-10/mo) | Mittel (Brand-Verwirrung) |
| **AnkiApp / AlgoApp** | Cloud-First Closed-Source | proprietär | Limited | Subscription (Details schwammig) | Gering (Reputation kaputt) |
| **Quizgecko** | AI-First (Quizzes, Podcasts) | proprietär | 1 AI-Lesson/Monat | $16/mo (Pro), $29 (Ultra) | Mittel (AI-Side) |
| **Knowt** | „Free Quizlet-Alternative" + AI | proprietär | Sehr großzügig | $9.99/mo (Ultra) | **Hoch** (gleiches Spielfeld) |
| **Wisdolia** | Browser-Ext: Karten aus Webcontent | proprietär | 50 Sets/Monat | $2.50/mo, $25/Jahr | Gering |
| **Mnemosyne** | Open-Source, Forschungs-Datasammlung | GPL | Voll gratis | — | Sehr gering |
| **Traverse** | Mind-Maps + SR (Mandarin Blueprint) | proprietär | Free-Plan | $15/mo Member, $35/User Enterprise | Gering |
| **Cerego** | Enterprise B2B Adaptive Learning | proprietär | — | ab $8.33/mo Indiv., Enterprise on req. | Sehr gering (B2B) |
| **NeuraCache** | Notion/Obsidian-Sync für SR | proprietär | Limited | 14d Trial → Pro (Preis nicht klar dokumentiert) | Gering |
> Threat-Ranking: nur **Anki, Quizlet, Mochi, Knowt** sind Top-Bedrohungen für Cardeckys Kernzielgruppe. RemNote, Quizgecko, AnkiPro/Noji sind Nebenfront.
---
## 3. Detail-Sektion pro Konkurrent
### 3.1 Anki (Desktop / AnkiWeb / AnkiDroid / AnkiMobile)
- **URL:** https://apps.ankiweb.net/
- **Plattformen:** Windows, macOS, Linux (Desktop), Web (AnkiWeb), Android (AnkiDroid), iOS (AnkiMobile)
- **USP:** Der etablierte technische Standard für Spaced Repetition; mächtig, erweiterbar (Add-ons), FSRS v6 nativ, riesiges Deck-Ökosystem (insbes. Medizin: AnKing).
- **Lizenz:** AGPL-3.0 (Desktop, AnkiDroid, Web). AnkiMobile iOS proprietär (finanziert die Open-Source-Arbeit).
- **Kosten:** Desktop / Web / Android **kostenlos**. AnkiMobile iOS: **$24.99-29.99 einmalig (Lifetime)**. AnkiHub-Cloud-Decks: $5/Monat (separat).
- **User loben:** Mächtig & flexibel; FSRS-Wirksamkeit; freie Decks (insbes. AnKing Step Deck mit 100k+ Studenten); Dauerhaftigkeit (seit 2006).
- **User kritisieren:** Steile UX-Lernkurve; FSRS-„Difficulty Hell" (Karten reifen langsam, Reviews explodieren); Plugin-Brüche zwischen Versionen; iOS-Preis abschreckend; Sync-Setup für Bilder/Audio umständlich.
- **Firma & Geschichte:** Damien Elmes (Australien), gestartet 5.10.2006 ursprünglich für Japanisch-Lernen. Im **Februar 2026** angekündigt, dass AnkiHub (Austin, TX) Business-Operations und Open-Source-Stewardship übernimmt — Anki bleibt Open Source, keine externen Investoren, Versprechen „no enshittification".
- **Bedrohungsgrad: Hoch.** Power-User-Standard, riesiges Decks-Ökosystem, kostenlos. Wir können sie nicht im technischen Spielfeld schlagen — wir müssen über UX, Onboarding und „Anki-Import-Bridge" gewinnen.
Quellen: [Anki Wikipedia](<https://en.wikipedia.org/wiki/Anki_(software)>) · [AnkiMobile App Store](https://apps.apple.com/us/app/ankimobile-flashcards/id373493387) · [Class Central: Anki founder steps back](https://www.classcentral.com/report/anki-founder-steps-back/) · [AnkiHub](https://www.ankihub.net/) · [Difficulty Hell in Anki](https://skerritt.blog/difficulty-hell-in-anki/) · [Anki FSRS-Forum](https://forums.ankiweb.net/c/fsrs/41)
---
### 3.2 Quizlet
- **URL:** https://quizlet.com/
- **Plattformen:** Web, iOS, Android.
- **USP:** Massen-Marktführer mit der größten Bibliothek shared decks (Schul-/Hochschul-Vokabeln); inzwischen stark AI-fokussiert (Q-Chat, Magic Notes, Coconote-Akquisition Feb 2026, ChatGPT-Integration März 2026).
- **Lizenz:** Proprietär.
- **Kosten:** Free (sehr eingeschränkt + Werbung). **Plus: $35.99/Jahr (~$2.99/mo)**. **Plus Unlimited: ~$44.99/Jahr** (entfernt Limits wie 3 Practice Tests/Monat, 20 Learn-Runden/Monat).
- **User loben:** Riesige Library shared sets; einfacher Einstieg; Multi-Device gut etabliert; AI-generierte Practice-Tests sind brauchbar.
- **User kritisieren:** **Trustpilot 1.4/5** aus 500+ Reviews. Aggressives Paywalling von Features, die früher gratis waren (Learn-Mode, Test, Lernrunden-Limit); Werbeflut im Free-Tier; Export-Möglichkeiten eingeschränkt (Lock-in); Bugs.
- **Firma & Geschichte:** 2005 gegründet von Andrew Sutherland (damals 15, Albany High School CA) für eigene Französisch-Vokabeln. Bootstrap bis 2015, dann $12M USV/Costanoa. 2020 $30M Series C bei General Atlantic, **$1B Bewertung**. Sitz San Francisco. Insgesamt ~$62M raised. CEO seit 2022 Lex Bayer. **Februar 2026: Akquisition Coconote**.
- **Bedrohungsgrad: Hoch (Reichweite), aber verwundbar.** Das Trustpilot-Desaster ist eine Steilvorlage. Unsere Chance: Quizlet-Refugees mit „so gut wie früher Quizlet, dazu FSRS und ohne Paywall-Gärten" abholen.
Quellen: [Quizlet Wikipedia](https://en.wikipedia.org/wiki/Quizlet) · [Trustpilot Quizlet (1.4/5)](https://www.trustpilot.com/review/www.quizlet.com) · [Quizlet $1B Bewertung TechCrunch](https://techcrunch.com/2020/05/13/quizlet-valued-at-1-billion-as-it-raises-millions-during-a-global-pandemic/) · [Crunchbase Quizlet](https://www.crunchbase.com/organization/quizlet) · [Navigating Quizlet's Controversial Changes](https://medium.com/@maxtan0626/navigating-quizlets-controversial-changes-afeb97aafd1e)
---
### 3.3 RemNote
- **URL:** https://www.remnote.com/
- **Plattformen:** Web, macOS, Windows, iOS, Android.
- **USP:** Hybrid aus Outliner-Notetaking (Roam-/Logseq-ähnlich) und integrierten SR-Karten — Karten entstehen direkt im Notiz-Flow via `::`-Syntax. Plus PDF-Annotation und Image-Occlusion.
- **Lizenz:** Proprietär.
- **Kosten:** Free (3 PDF-Annotationen, 5 Image-Occlusion-Karten). **Pro: $8/Monat annual ($96/Jahr)** oder $10/mo monthly.
- **User loben:** Notes + Cards in _einem_ Workflow; flexible nested Outline-Struktur; PDF-Annotation; AI-Generierung aus Notizen/PDFs.
- **User kritisieren:** Steile Lernkurve („nichts versteht man in 10 Min"); UI als überladen empfunden; **Performance-Probleme** (langsam beim Laden großer Datenbanken, iPad-Stabilität); Bugs nach Beta-Updates; non-English-Support schwach.
- **Firma & Geschichte:** Gegründet 2019 von Martin Schneider (MIT) und Moritz Wallawitsch (Berlin, HTW). Sitz: USA. **$2.8M Seed (Sept 2021)** unter General Catalyst. Hat 2025 ~$2M Revenue mit ~18 Personen erreicht.
- **Bedrohungsgrad: Mittel.** Andere Zielgruppe (PKM-Power-User, Studenten, die Notes wollen). Cardecky ist fokussierter — wir müssen die „nur-Karten"-Nische gegen ihre Hybrid-Erweiterung verteidigen.
Quellen: [RemNote Pricing](https://www.remnote.com/pricing) · [Crunchbase RemNote](https://www.crunchbase.com/organization/remnote) · [RemNote Reviews Product Hunt](https://www.producthunt.com/products/remnote/reviews) · [RemNote Performance-Forum](https://forum.remnote.io/t/remnote-is-my-dream-pkm-yet-its-too-slow-am-i-doing-something-wrong/10920) · [Latka RemNote $2M ARR](https://getlatka.com/companies/remnote.com)
---
### 3.4 Mochi
- **URL:** https://mochi.cards/
- **Plattformen:** macOS (Intel/AS), Windows, Linux Desktop, iOS, Android, Web.
- **USP:** Markdown-First, sauberes minimalistisches UI, Local-First mit Offline-Support; Image-Occlusion und Anki-`.apkg`-Import als First-Class-Feature ohne Plugin-Frickelei. **FSRS seit Mid-2025 unterstützt.**
- **Lizenz:** Proprietär.
- **Kosten:** Free (Single-Device, alle Karten lokal). **Pro: $5/Monat** (Sync, mehrere Geräte, Translations).
- **User loben:** „schönes" UI; intuitiver als Anki; sofortiges Karten-Lernen ohne Onboarding-Friktion; Anki-Import als Plugin-Free-Feature; Markdown-Workflow.
- **User kritisieren:** Sync nur in Pro (Single-Device-Free fühlt sich begrenzt an); algorithm war bis Mid-2025 schwächer als Anki (FSRS-Beta hat das gefixt); kleinere Community → weniger shared decks; Solo-Developer (Bus-Faktor).
- **Firma & Geschichte:** Solo-Projekt von **Matthew Steedman**, eigenfinanziert, Forum auf forum.mochi.cards. Keine externen Investoren öffentlich bekannt.
- **Bedrohungsgrad: Hoch — direktester Wettbewerber.** Praktisch identische Positionierung (Markdown, schickes UI, modern, FSRS, Local-First). Unterschied: Mochi nimmt $5/mo für Sync — **wir bieten Sync gratis**, das ist unsere stärkste objektive Differenzierung gegen Mochi.
Quellen: [Mochi Cards](https://mochi.cards/) · [Mochi App Store](https://apps.apple.com/us/app/mochi-flashcards-and-notes/id1507775056) · [First Impressions of Mochi (borretti.me)](https://borretti.me/article/first-impressions-mochi) · [Bunpro: If you don't like Anki, try Mochi](https://community.bunpro.jp/t/if-you-dont-like-anki-consider-giving-mochi-a-try/59955) · [Mochi Changelog](https://mochi.cards/changelog/)
---
### 3.5 Brainscape
- **URL:** https://www.brainscape.com/
- **Plattformen:** Web, iOS, Android.
- **USP:** „Confidence-Based Repetition" — User raten Selbsteinschätzung auf 1-5-Skala (statt SM-2/FSRS), als wissenschaftlich vermarktetes Schedule-System. Große kuratierte Decks-Library und EDU/Enterprise-Vertrieb.
- **Lizenz:** Proprietär.
- **Kosten:** Free (limitierter Zugang zu Deck-Bibliothek). **Pro: ~$19.99/Monat** (Discounts bei Jahres-/Lifetime-Plan). **Lifetime: $79.99**.
- **User loben:** Kuratierte Content-Bibliothek; klares Lernkonzept; schickes UI; gute Statistiken; Collaboration-Features für Teams.
- **User kritisieren:** Algorithmus weniger anpassbar als Anki/FSRS; Pro-Preis wird als hoch empfunden; Free-Tier-Decks sehr begrenzt; weniger Power-User-Features.
- **Firma & Geschichte:** Gegründet von Andrew Cohen (Idee 2006 Panama-Spanisch-Excel-Macro, später Master's Columbia EdTech). Sitz: New York. Founding Team: Cohen, Andy Lutz, Jay Stramel, Jonathan Thomas, Ron Cadet (2018). >$3M raised bis 2015.
- **Bedrohungsgrad: Gering-Mittel.** Andere Zielgruppe (Pro-Decks-Käufer, EDU-Markt). Wir konkurrieren wenig direkt.
Quellen: [Brainscape](https://www.brainscape.com/) · [G2 Brainscape Reviews](https://www.g2.com/products/brainscape/reviews) · [Brainscape Wikipedia](https://en.wikipedia.org/wiki/Brainscape) · [How Brainscape Was Born](https://www.brainscape.com/academy/how-brainscape-was-born/)
---
### 3.6 Memrise
- **URL:** https://www.memrise.com/
- **Plattformen:** Web, iOS, Android.
- **USP:** Sprachen-Fokus mit Native-Speaker-Videos und seit 2024/25 stark ausgebaute „AI Buddies" (Grammar Buddy, Translator Buddy, Culture Buddy, MemBot Chatbot auf GPT-Basis).
- **Lizenz:** Proprietär.
- **Kosten:** Free (limitiert + Ads). **Monthly $27.99**, **Annual $130.99 (~$11/mo)**, **Lifetime $199.99** (oft Discounts bis 50%).
- **User loben:** Native-Speaker-Video-Clips als Alleinstellungsmerkmal vs Duolingo; AI-Buddies bringen Konversationspraxis; gut für Vokabel-Aufbau.
- **User kritisieren:** Schwach in Grammatik; nicht für Fortgeschrittene; AI-Buddies hinter Paywall; teure Subscription verglichen mit Konkurrenten; legendäre community-„mems"-Funktion wurde entfernt (alte Community vergrätzt).
- **Firma & Geschichte:** Gegründet 2010 von **Ed Cooke** (Grand Master of Memory), **Ben Whately** und **Greg Detre** (Princeton-Neurowissenschaftler). Oxford-Trio. Sitz London. **$25.3M raised** über 7 Runden / 10 Investoren. Profitabel seit Ende 2016. **72M registrierte User (2024)**.
- **Bedrohungsgrad: Gering.** Sprach-Lerner-Nische, kaum Überlappung mit unserer generischen SR-Zielgruppe.
Quellen: [Memrise](https://www.memrise.com/) · [Memrise Wikipedia](https://en.wikipedia.org/wiki/Memrise) · [Crunchbase Memrise](https://www.crunchbase.com/organization/memrise) · [Business of Apps: Memrise Statistics 2026](https://www.businessofapps.com/data/memrise-statistics/)
---
### 3.7 SuperMemo
- **URL:** https://www.supermemo.com/ (Web/Mobile) · https://supermemo.store/ (Desktop)
- **Plattformen:** Windows Desktop (Premium-Version), Web, iOS, Android, Browser-API.
- **USP:** Originator des Spaced-Repetition-Konzepts (1985 ff.) — Algorithmen SM-2 bis aktuell **SM-20 (2026)**. Die Desktop-Version hat Funktionen, die andere SR-Tools nicht haben (Incremental Reading, Concept Maps).
- **Lizenz:** Proprietär.
- **Kosten:** Mobile/Web: 1 Monat free, danach **~9.90 USD/EUR pro Monat**. Desktop SuperMemo 19 (Windows): **~$66 perpetual** (Käufer März 2026 bekommen kostenloses Upgrade auf SuperMemo 20). API: Early Access, 100 Repetitions/Tag gratis.
- **User loben:** Algorithmus-Tiefe; Incremental Reading; SM-20 als state-of-the-art; Hardcore-Power-User-Tool.
- **User kritisieren:** UI „aus den 1990ern"; sperrige Bedienung; Desktop-only für viele Features; Mobile-App stark eingeschränkt; Preis vs Anki nicht zu rechtfertigen für 95% der User.
- **Firma & Geschichte:** SuperMemo World Sp. z o.o., gegründet **5. Juli 1991** in Poznań, Polen, von Krzysztof Biedalak und **Piotr Wozniak** (mit Tomasz Kuehn, Janusz Murakowski, Marczello Georgiew). Wozniak begann SuperMemo 1.0 schon 13.12.1987.
- **Bedrohungsgrad: Gering.** Nische für Algorithmus-Enthusiasten und Incremental-Reading-Fans. Keine reale UX-Bedrohung für uns.
Quellen: [SuperMemo Wikipedia](https://en.wikipedia.org/wiki/SuperMemo) · [SuperMemo Store](https://supermemo.store/products/supermemo-19-for-windows) · [Algorithm SM-18](https://supermemo.guru/wiki/Algorithm_SM-18) · [Piotr Wozniak](https://supermemo.guru/wiki/Piotr_Wozniak) · [SuperMemo iOS App Store](https://apps.apple.com/us/app/supermemo-effective-learning/id982498980)
---
### 3.8 AnkiPro / Noji
- **URL:** https://noji.io/ (vormals ankipro.net)
- **Plattformen:** iOS, Android, Web.
- **USP:** „Anki-Look-and-Feel" mit modernem UI und Cloud-Sync — verkauft sich aktiv als „die einfachere Anki-Variante". Nicht kompatibel mit echtem Anki (auch nicht mit `.apkg`-Decks ohne Workarounds).
- **Lizenz:** Proprietär.
- **Kosten:** Free mit Werbung/Limits. Pro-Subscription, Preise nicht prominent — nach Reports im Bereich **$5-10/mo** oder Jahresplan.
- **User loben:** Schickes Mobile-UI; einfacher Onboarding-Flow; Cross-Device-Sync „out of the box"; community Decks.
- **User kritisieren:** **Brand-Verwirrung** (User dachten, sie laden „echtes" Anki herunter); **10-Tage-Sync-Outage Mai 2025** mit Datenverlust für viele User; Lock-in (Export-Tools wurden vom Anbieter blockiert, ein Migrations-Tool erhielt einen **Rickroll-Response** von AnkiPro); offizielles Anki-Team distanziert sich.
- **Firma & Geschichte:** Anki Pro UAB; Co-Founder **Maksim Abramchuk** (im Crunchbase) und **Andrew Bond** (LinkedIn). 2021 gestartet, 2024/25 Rebrand zu **Noji**. Sitz nicht eindeutig öffentlich (LinkedIn-Indikatoren UK/Osteuropa).
- **Bedrohungsgrad: Mittel.** Nicht weil sie technisch besser sind, sondern weil Anki-Suchende auf sie reinfallen. **Lehre für Cardecky: Brand-Hygiene**. Wir sind „Cardecky" — nie „Anki" im Marketing, klare Trennung kommunizieren, Anki-Import sauber als Bridge dokumentieren.
Quellen: [Anki knockoffs (offizielle Anki FAQ)](https://faqs.ankiweb.net/anki-knockoffs.html) · [AnkiPro Ripoff Forum](https://forums.ankiweb.net/t/ankipro-another-ripoff-anki-app/11791) · [Anki Users Get Rickrolled](https://broderic.blog/post/anki-users-get-rickrolled/) · [Noji App Store](https://apps.apple.com/us/app/noji-flashcards-anki-method/id1573585542) · [Crunchbase Anki Pro](https://www.crunchbase.com/organization/anki-pro) · [Speakada: Official Anki vs Fake Apps](https://speakada.com/official-anki-vs-fake-apps-the-critical-mistake-costing-language-learners-hours/)
---
### 3.9 AnkiApp / AlgoApp
- **URL:** https://www.algoapp.ai/ (vormals ankiapp.com)
- **Plattformen:** iOS, Android, Web, Desktop.
- **USP:** Closed-Source Cloud-First Karten-App, die seit Jahren den Namen „Anki" ausnutzt. **In manchen Regionen (z. B. japanischer App Store) firmiert sie weiterhin als „AnkiApp"**.
- **Lizenz:** Proprietär.
- **Kosten:** Free + Subscription-Tiers (Details vage, oft als „Trial-Trap" kritisiert).
- **User loben:** Funktioniert auf allen Plattformen; Cloud-Sync inkludiert; einfaches UI.
- **User kritisieren:** **Komplette Brand-Täuschung**; kein Import/Export zu echtem Anki; aggressive Subscription-Walls; Reviews mit „nichts mit echtem Anki zu tun" als wiederkehrendes Muster; Reputation in der Community unter null.
- **Firma & Geschichte:** AlgoApp Inc., gegründet **2021**, Sitz **San Mateo, CA**. Vor Kurzem von AnkiApp zu AlgoApp umbenannt (Anki-Brand-Druck wurde zu groß), aber teils noch unter altem Namen aktiv.
- **Bedrohungsgrad: Gering.** Reputation kaputt; informierte User meiden sie aktiv. Hauptthema für uns ist nicht Wettbewerb, sondern Brand-Hygiene-Lehre (siehe AnkiPro).
Quellen: [Anki knockoffs FAQ](https://faqs.ankiweb.net/anki-knockoffs.html) · [AlgoApp on Anki Forum](https://forums.ankiweb.net/t/algoapp-still-using-ankiapp-name-in-japanese-app-store/69103) · [Crunchbase AlgoApp](https://www.crunchbase.com/organization/algoapp) · [Pitchbook AlgoApp](https://pitchbook.com/profiles/company/495884-44)
---
### 3.10 Quizgecko
- **URL:** https://quizgecko.com/
- **Plattformen:** Web, iOS, Android.
- **USP:** AI-First-Workflow: aus PDF / Text / URL → Quizzes + Karten + Notizen + **Audio-Podcasts** (Notebook-LM-ähnlich). SR ist sekundär.
- **Lizenz:** Proprietär.
- **Kosten:** **Basic Free (1 AI-Lesson/Monat)**. **Pro $16/mo** (annual). **Ultra $29/mo** (50 Podcasts/mo, Custom Prompts). Business $32/mo (API + Branding).
- **User loben:** Vielseitige Output-Formate (Quiz/Karten/Podcast); guter PDF-Parser; multi-Question-Types.
- **User kritisieren:** SR ist „mitgeliefert" aber nicht der Fokus; Free-Tier sehr eng (1 Lesson); Pro-Preis hoch verglichen mit dedicated AI-Card-Tools.
- **Firma & Geschichte:** Privates Startup, kleinere Bekanntheit, keine prominente Funding-Information öffentlich.
- **Bedrohungsgrad: Mittel (in der AI-Front).** Wir konkurrieren am AI-Generierungs-Feature. Für reines SR-Lernen ist Quizgecko keine Bedrohung; für „ich habe ein Skript und will lernen" schon. Unser Konter: AI-Generierung ist bei uns „free with sync" und dann _dauerhaft_ in einem echten SR-System.
Quellen: [Quizgecko](https://quizgecko.com/) · [Quizgecko Pricing](https://quizgecko.com/pricing) · [Toosio Quizgecko Review 2026](https://toosio.com/tool/quizgecko-ai-quiz-flashcard-podcast-generator)
---
### 3.11 Knowt
- **URL:** https://knowt.com/
- **Plattformen:** Web, iOS, Android.
- **USP:** Positioniert sich explizit als **„free Quizlet alternative"**. Importiert Quizlet-Sets direkt, hat ähnliche Study-Modes (Learn, matching, practice tests, „Knowt Play") plus AI-Generierung aus Notizen/PDFs.
- **Lizenz:** Proprietär.
- **Kosten:** **Sehr großzügiges Free-Tier** (unlimited Karten, alle Study-Modes, basic AI mit monatlichen Limits). **Ultra: $9.99/mo annual** (Snap & Solve, unlimited AI). Manche Listen nennen einen $12.50/mo Premium.
- **User loben:** „Endlich Quizlet ohne Paywall"; Quizlet-Import funktioniert; AI-Note-zu-Karten brauchbar; Free-Tier wirklich nutzbar.
- **User kritisieren:** Hauptsächlich Schüler-/US-Highschool-Zielgruppe (für Erwachsene weniger durchdacht); AI-Limits im Free-Tier; SR-Algorithmus weniger ausgereift als Anki/FSRS.
- **Firma & Geschichte:** US-Startup, primär Studenten-Zielgruppe, keine prominente Funding-Information öffentlich verfügbar.
- **Bedrohungsgrad: Hoch (gleiches Spielfeld).** Beide Apps positionieren „free + AI + bessere UX als Quizlet". Unsere Differenzierung: **FSRS v6, Markdown, echtes Local-First-PWA-Modell, Anki-Import inkl. Bilder/Audio**. Knowt ist webbasiert, wir sind installierbar offline-first.
Quellen: [Knowt](https://knowt.com/) · [Knowt vs Quizlet (StudyGenie 2026)](https://studygenie.io/blog/knowt-vs-quizlet) · [Best Quizlet Alternatives 2026](https://kvistly.com/blog/best-quizlet-alternatives)
---
### 3.12 Wisdolia
- **URL:** https://www.wisdolia.com/ (vorrangig als Chrome-Extension)
- **Plattformen:** Chrome Extension; Karten-Export zu Anki möglich.
- **USP:** Generiert Karten aus _jeder Webseite, PDF oder YouTube-Video_ in Sekunden — sehr fokussiert auf den „Capture beim Browsen"-Use-Case.
- **Lizenz:** Proprietär.
- **Kosten:** **Free: 50 Sets/Monat** (Limit: 15 PDF-Seiten, 12 Min YouTube). **Pro: $2.50/mo oder $25/Jahr** (unlimited).
- **User loben:** Spielerisch billig; Browser-Extension-Workflow ist reibungsarm; Anki-Export als Bridge.
- **User kritisieren:** Kein eigenes SR-System mit eigener Tiefe (eher Generator als Lern-App); Browser-only beschränkt.
- **Firma & Geschichte:** Kleines indie-Projekt; keine prominente Funding-Information öffentlich.
- **Bedrohungsgrad: Gering.** Komplementäres Tool eher als Wettbewerber — wer Wisdolia nutzt, exportiert oft _zu Anki_ (oder zu uns, wenn wir Wisdolia-Export sauber importieren).
Quellen: [Wisdolia (Findmyaitool)](https://findmyaitool.com/tool/wisdolia) · [Wisdolia Plain English Walkthrough](https://plainenglish.io/artificial-intelligence/wisdolia-ai-generate-flashcards-anywhere-on-the-web-with-google-chrome-extension)
---
### 3.13 Mnemosyne
- **URL:** https://mnemosyne-proj.org/
- **Plattformen:** Windows, macOS, Linux Desktop; Android (eingeschränkt).
- **USP:** Open-Source-Alternative zu Anki mit explizitem **Forschungs-Fokus**: Nutzer können (opt-in) anonyme Lerndaten beitragen, die seit 2006 zur Untersuchung von Langzeitgedächtnis gesammelt werden.
- **Lizenz:** GPL.
- **Kosten:** Komplett gratis. Kein Sync.
- **User loben:** Sauber, leichtgewichtig, ehrlich akademisch; gut für Forschung; lange Geschichte (>20 Jahre).
- **User kritisieren:** UI veraltet; Mobile-Support schwach (Android-App OK, iOS quasi nichts); kleine Community; weniger Decks als Anki.
- **Firma & Geschichte:** Community-Projekt um Peter Bienstman (Belgien). Letzte Release März 2026 — aktiv aber langsam.
- **Bedrohungsgrad: Sehr gering.** Akademisches Nischen-Tool, andere Zielgruppe.
Quellen: [Mnemosyne Wikipedia](<https://en.wikipedia.org/wiki/Mnemosyne_(software)>) · [Mnemosyne Project](https://mnemosyne-proj.org/) · [GitHub Mnemosyne](https://github.com/mnemosyne-proj/mnemosyne)
---
### 3.14 Traverse
- **URL:** https://traverse.link/
- **Plattformen:** Web, iOS, Android.
- **USP:** Kombiniert Mind-Mapping + Note-Taking + SR-Karten in einer App; offizielle Integration mit „Mandarin Blueprint" (Chinesisch-Lernkurs).
- **Lizenz:** Proprietär.
- **Kosten:** Free, **Member $15/mo**, **Enterprise $35/User/mo**.
- **User loben:** Mind-Map + Karten kombiniert ist konzeptionell stark für Sprachen/komplexe Domains; Mandarin-Community schätzt es.
- **User kritisieren:** Member-Preis hoch; relativ kleine Bekanntheit außerhalb Mandarin-Sub-Community; nicht so viel feature parity mit Anki.
- **Firma & Geschichte:** Indie-Startup, primär Bootstrap; keine prominente Funding-Information öffentlich.
- **Bedrohungsgrad: Gering.** Andere Zielgruppe (visuelles Lernen, Sprachen). Keine direkte Konkurrenz.
Quellen: [Traverse.link](https://traverse.link/) · [Traverse.link Capterra 2026](https://www.capterra.com/p/234102/Traverse/)
---
### 3.15 Cerego
- **URL:** https://www.cerego.com/
- **Plattformen:** Web (B2B-Plattform).
- **USP:** Enterprise-Adaptive-Learning mit Versprechen „4-5× schnelleres Lernen, 90% Retention"; AI/ML-basierte Personalisierung. **Verkauft sich an Unternehmen, nicht Endkunden**.
- **Lizenz:** Proprietär.
- **Kosten:** Indiv. ab **$8.33/mo**, Enterprise ab 500 Seats individuell verhandelt (nicht öffentlich).
- **User loben:** Solide Lerneffekte in Enterprise-Trainings; gutes Reporting; sauberes UI.
- **User kritisieren:** Nicht für Selbstlerner gemacht; teuer für Einzelne; deck-Erstellungs-Workflow umständlich für Privatuser.
- **Firma & Geschichte:** US-Firma, in der Vergangenheit mehrfach pivotiert (B2C → B2B). Keine aktuelle Funding-Info.
- **Bedrohungsgrad: Sehr gering.** B2B, andere Welt.
Quellen: [Cerego](https://www.cerego.com/) · [Cerego G2](https://www.g2.com/products/cerego/reviews) · [Cerego Capterra 2026](https://www.capterra.com/p/169739/Cerego/)
---
### 3.16 NeuraCache
- **URL:** https://neuracache.com/
- **Plattformen:** iOS, Android.
- **USP:** SR-Karten **synchronisiert mit Notion / Obsidian / Logseq / Roam / Evernote / OneNote**, automatisches Extrahieren markierter Notizen → Karten. „Bridge"-Tool für PKM-Nutzer.
- **Lizenz:** Proprietär.
- **Kosten:** 14-Tage-Trial Pro. Pro-Subscription oder One-Time Lifetime; konkrete 2026-Preise nicht klar dokumentiert auf der öffentlichen Seite.
- **User loben:** Notion-/Obsidian-Sync ist die Killer-Funktion; spart Doppelarbeit für PKM-Power-User.
- **User kritisieren:** Klein, indie; UI weniger poliert als Mochi; Pricing intransparent; eher Mobile-only.
- **Firma & Geschichte:** Indie-Developer, geringe öffentliche Sichtbarkeit.
- **Bedrohungsgrad: Gering.** PKM-Nische; keine Überlappung mit unserer Generalist-Zielgruppe.
Quellen: [NeuraCache](https://neuracache.com/) · [NeuraCache App Store](https://apps.apple.com/us/app/neuracache-spaced-repetition/id1450923453) · [NeuraCache AlternativeTo](https://alternativeto.net/software/neuracache/about/)
---
## 4. Schluss-Empfehlung: 3 Differenzierungs-Hebel für Cardecky
### Hebel 1: **„Free Sync" konsequent ausspielen**
Niemand sonst bietet die Kombination, die wir liefern — _Markdown + FSRS + Multi-Device-Cloud-Sync inkl. Bilder/Audio + PWA + AI-Generierung_, alles im Free-Tier. Konkurrenten wollen für Sync Geld:
- Mochi: $5/mo
- AnkiMobile iOS: $25-30 einmalig
- Quizlet: Sync ja, aber Features paywallen
- RemNote: Pro-Limit (3 PDFs)
- Brainscape: $20/mo
**Action:** Marketing-Hauptbotschaft auf Pricing-Seite und Landingpage explizit machen: _„Sync gratis, immer. Karten gehören dir, lokal und in der Cloud."_ Gegen Mochi besonders direkt vergleichen. Wenn wir später monetarisieren, sollte Sync NIE in den Pro-Tier wandern — unser Reputations-Anker.
### Hebel 2: **Anki-Migration als First-Class-Feature, ohne Brand-Sniping**
Anki bleibt Power-User-Standard, aber Anki-User klagen über UX, FSRS-Tweaking und iOS-Preis. Sie sind die wertvollste Migrations-Zielgruppe (lange Lern-Historie, 100k+ Karten). Wir importieren bereits inkl. Bilder/Audio — das ist Gold.
**Action:**
- Eine dezidierte Landingpage `cardecky.com/from-anki` mit ehrlichem Vergleich (was wir besser machen, was Anki noch besser kann), Migrationsanleitung, und expliziter Distanzierung von AnkiPro/AnkiApp/Noji.
- Eine ehrliche Story dazu („Wir sind nicht Anki. Wir sind Cardecky. Aber wir respektieren deine Anki-Karten."). Das positioniert uns als seriöse Alternative gegen die Brand-Sniper.
- Für Bonus-Punkte: Imports von Mochi-Decks und Quizlet-Sets ebenfalls anbieten — Knowt lebt davon, wir können das auch.
### Hebel 3: **„Local-First PWA" als Tech-Identität, nicht nur Implementierungsdetail**
Cardeckys Local-First + PWA-Architektur ist konzeptionell anders als Quizlet/Knowt (Web-First) und besser als Mochi auf iOS (App-Store-Friktion). Wir sind installierbar, offline-funktional, ohne App Store. Das schlägt mehrere Fliegen:
- Kein iOS-30%-Tax (vs AnkiMobile-Modell, das deshalb $25 kostet)
- Kein Vendor-Lock-in (Daten bleiben im Browser/lokal nutzbar)
- Kein Werbe-Modell nötig (vs Quizlet)
- Schnelles Auto-Update (vs Anki-Plugin-Brüche)
**Action:** Konsequent „Local-First PWA" in Tech-Marketing nutzen (HN, Reddit /r/Anki, /r/medicalschool, indie-hacker-Communities). Genau dort sitzen Quizlet-Wechsler und Anki-frustrierte Med-Studenten, die diesen technischen Pitch verstehen.
---
## Bonus: Was wir _nicht_ tun sollten
- **Nicht „Anki" im Namen führen** — siehe AnkiPro/AnkiApp Reputation. „Cardecky" ist neutral, freundlich, und distanziert sich klar.
- **Nicht die SR-Algorithmus-Race spielen** — FSRS v6 reicht. SuperMemo SM-20 ist kein Marketing-Argument für 99% der User.
- **Nicht in Sprach-Lernen pivotieren** — Memrise und Duolingo besitzen das Feld, andere Mechaniken nötig.
- **Nicht alle AI-Features paywallen** — Knowt zeigt: ein großzügiges Free-Tier mit AI ist der Hebel gegen Quizlet.
- **Nicht Sync paywallen** — siehe Hebel 1. Das ist unser Anker-Wert.
---
## Methodische Hinweise
- Recherche durchgeführt 2026-05-07 via WebSearch (offizielle Pricing-Seiten, G2, Trustpilot, Capterra, Crunchbase, Wikipedia, Reddit, Anki-Forums, Hacker News).
- Einige Konkurrenten (NeuraCache, Quizgecko, Traverse, kleinere Indie-Tools) haben begrenzt öffentlich verfügbare Daten zu Funding/Team — wo Daten fehlen, ist „nicht öffentlich bekannt" eingetragen statt Spekulation.
- AnkiPro/Noji ist besonders intransparent (eigene Pricing-Seite versteckt klare Tier-Liste, Zahlen aus Reviews); wir sollten das im Auge behalten, wenn wir gegen sie konkurrieren.
- Quizlet-Bewertung mit „verwundbar" basiert real auf dem **Trustpilot-1.4/5** und der breiten Reddit-Stimmung — das ist eine echte Marktchance, kein Wunschdenken.

View file

@ -0,0 +1,367 @@
# Cardecky — Projekt-Leitlinien
Verbindliche Regeln für den Spinoff. Ziel: in wenigen Wochen ein
ausspielbares Web-MVP, das ausschließlich seinen *Core Gameloop*
beherrscht und alles andere von zentralen Mana-Bausteinen erbt.
**Status:** Planungsphase, noch kein Code.
**Name:** Cardecky.
**App-Domain:** `cardecky.mana.how` (Subdomain unter `*.mana.how`, SSO über mana-auth).
**Marketing-Landing:** `cardecky.com` (eigene Domain, statisch, SEO/Akquise — keine Auth, leitet auf `cardecky.mana.how` für die App).
**Zugang:** offen für jeden eingeloggten Mana-User (`requiredTier: 'public'`, kein Beta-Gate).
## 1. Mission in einem Satz
Die schönste, einfachste Karteikarten-App mit Spaced Repetition —
zuerst nur Web, später Mobile, KI-Generierung als Phase 2.
## 2. Game-Dev-Prinzip: zuerst nur der Core Gameloop
Wie bei einem Spielprototyp gilt: alles, was nicht zum Loop gehört,
wird zurückgestellt. Erst wenn der Loop sich gut anfühlt und Nutzer ihn
freiwillig wiederholen, wird gebaut, was drumherum gehört.
### Der Core Gameloop von Cardecky
```
Start
"Du hast N Karten heute fällig" ─────► (wenn 0: "Alles gelernt — komm später wieder")
[Lernen starten]
Vorderseite zeigen ──► User denkt ──► Tap/Space ──► Rückseite zeigen
Selbst-Bewertung: 1=nochmal · 2=schwer · 3=gut · 4=leicht
FSRS rechnet next-due ──► nächste Karte (oder Session-Ende)
Session-Ende: "X Karten gelernt, nächste in Y Stunden"
└─► zurück zum Start
```
Sekundäre Loops (Karten erstellen, Decks verwalten) werden gebaut, sind
aber UI-arm. **Tertiäre Loops (KI-Generierung, Voice, Sharing) sind
Phase 2 und werden in Phase 1 nicht angefasst.**
### Was Phase 1 enthält
- Decks anlegen / löschen / umbenennen
- Karten manuell erstellen (Markdown-Inhalt)
- **Kartentypen:** Basic, Basic + Reverse, Cloze, Type-In (siehe §6)
- Lernsession mit FSRS v6, **inklusive per-User-Parameter-Tuning**
- "Heute fällig"-Übersicht + Streak-Zähler
- Tags auf Decks (das Modul hat sie ohnehin schon, raus wäre Mehrarbeit)
- PWA-installierbar, offline-fähig
- Auth via mana-auth, Sync via mana-sync
### Was Phase 1 absichtlich NICHT enthält
- KI-Generierung von Karten (kein PDF-Upload, keine Bild→Karte)
- Voice/TTS-Lernen
- Anki-Import / Export
- Statistik-Dashboards (nur Streak + Tagessumme)
- Public Decks / Marktplatz / Sharing
- Stripe / Bezahlung
- Mobile-App (PWA-tauglich aber kein Expo)
- Eigene Domain & Marketing-Landing
- Mehrsprachigkeit über Deutsch hinaus
- Bilder / Audio in Karten
- Image-Occlusion-Karten, Audio-Karten, Multiple-Choice
- Custom Card-Templates / WYSIWYG-Editor
- Erweiterte Suche
Jede dieser Features ist legitim — aber nur, wenn der Loop steht.
## 3. Goldene Regeln
1. **Simpel schlägt vollständig.** Wenn ein Feature nicht zum Core Gameloop gehört, kommt es in einen Phase-2-Backlog, nicht in den Code.
2. **Open Source only.** Jede Library, jedes Tool, jeder Dienst muss eine OSI-konforme Lizenz haben (MIT, Apache 2.0, BSD, MPL, AGPL akzeptabel). Keine Closed-Source-SDKs, keine proprietären APIs als Pflichtabhängigkeit.
3. **Bevorzugt was im Verein schon läuft.** Neue Technologie nur einführen, wenn ein konkreter Engpass es verlangt und kein vorhandenes Tool es löst.
4. **Zentrale Mana-Dienste statt Eigenbau.** Auth, Sync, Analytics, Notifications, Media usw. werden NICHT neu gebaut — siehe §5.
5. **Local-First wie der Rest des Verein-Stacks.** IndexedDB als Quelle der Wahrheit, Sync nach Postgres im Hintergrund.
6. **`cardecky.mana.how` als Subdomain unter `*.mana.how`.** Kein eigenes Auth-System, kein eigenes Hosting-Setup — Eintrag in `PRODUCTION_TRUSTED_ORIGINS` + Cloudflare-Tunnel-Route reichen.
7. **Eine UI-Schicht, ein Theme.** Wir verwenden `@mana/shared-theme(-ui)` und `@mana/shared-ui` so weit es geht — kein paralleles Design-System.
8. **Erweiterbare Daten, simples UI.** Das Datenmodell denkt zukünftige Kartentypen mit (siehe §6), das UI zeigt in Phase 1 nur die vier definierten Typen.
## 4. Tech-Stack (Phase 1)
Alles bereits im Verein verwendet, alles OSI-Open-Source.
### Frontend
| Schicht | Wahl | Lizenz |
|---|---|---|
| Framework | SvelteKit 2 | MIT |
| UI-Sprache | Svelte 5 (Runes) | MIT |
| Sprache | TypeScript 5 | Apache-2.0 |
| Styling | Tailwind CSS 4 | MIT |
| Build/Dev | Vite | MIT |
| PWA | `@vite-pwa/sveltekit` (über `@mana/shared-pwa`) | MIT |
| Icons | über `@mana/shared-icons` | MIT |
| Markdown-Render | `marked` + `DOMPurify` | MIT |
### Datenhaltung (Client)
| Schicht | Wahl | Lizenz |
|---|---|---|
| Local Store | IndexedDB via Dexie | Apache-2.0 |
| Local-Store-Wrapper | `@mana/local-store` (intern) | — |
| Verschlüsselung | AES-GCM-256 via `@mana/shared-crypto` (Phase 2 — Hooks bereits an allen Schreib-/Lese-Pfaden, Wirkung deferred bis Vault-Server-Roundtrip steht; siehe `src/lib/data/crypto.ts`) | — |
### Spaced Repetition
| Schicht | Wahl | Lizenz |
|---|---|---|
| Algorithmus | FSRS v6 (Free Spaced Repetition Scheduler) | BSD-3 |
| TS-Implementation | `ts-fsrs` (offizielle Portierung, mit Optimizer) | MIT |
| Per-User-Tuning | `ts-fsrs`-Optimizer, läuft client-seitig nach ≥ 50 Reviews | MIT |
### Deployment
| Schicht | Wahl | Lizenz |
|---|---|---|
| Adapter | `@sveltejs/adapter-node` | MIT |
| Container | Docker, hinter Cloudflare Tunnel | Apache-2.0 |
| Host | Mac mini (siehe `docker-compose.macmini.yml`) | — |
### Tooling
| Schicht | Wahl | Lizenz |
|---|---|---|
| Paket-Manager | pnpm 9 | MIT |
| Monorepo-Orchestrierung | Turborepo (vorhanden) | MPL-2.0 |
| Linting | ESLint (`@mana/eslint-config`) | MIT |
| Formatierung | Prettier | MIT |
| Tests (Unit) | Vitest | MIT |
| Tests (E2E) | Playwright | Apache-2.0 |
| TS-Config | `@mana/test-config`, `@mana/shared-vite-config` | — |
### Backend in Phase 1: keiner
Phase 1 braucht **keinen eigenen Service**. Lese-/Schreibpfad geht
ausschließlich über IndexedDB → `mana-sync` (existiert) → Postgres.
Erst wenn KI-Generierung (Phase 2) dazukommt, entsteht
`services/cards-server` (Hono + Bun, analog zu allen anderen
Verein-Services).
## 5. Zentrale Mana-Bausteine (Pflicht in Phase 1)
### Services (laufen bereits, nur konsumieren)
| Service | Port | Wofür in Cardecky |
|---|---|---|
| `mana-auth` | 3001 | SSO, JWT, Sessions, Tier-Claims. Cardecky-Origin in `PRODUCTION_TRUSTED_ORIGINS` eintragen. |
| `mana-sync` | 3050 | Sync der `cards`-AppId-Daten (Decks, Karten, Reviews, StudyBlocks). |
| `mana-user` | 3062 | Profilinfos / Settings. |
| `mana-analytics` | 3064 | Page-Views, Loop-Events (siehe §11). |
| `mana-events` | 3115 | Domain-Events für Streak-Logik. |
| `mana-notify` | 3040 | "Du hast X Karten fällig"-Push (Phase 1.5). |
| `mana-credits` | 3061 | **Erst Phase 2** (KI-Generierung). |
| `mana-subscriptions` | 3063 | **Erst Phase 2** (Pro-Tier). |
| `mana-llm`, `mana-stt`, `mana-tts` | | **Erst Phase 2.** |
| `mana-media` | 3015 | **Erst wenn Bilder in Karten erlaubt sind.** |
### Workspace-Pakete (`@mana/*`)
| Paket | Wofür in Cardecky |
|---|---|
| `@mana/shared-auth` | Client-seitiger Auth-Hook (SSO-Flow, JWT-Handling). |
| `@mana/shared-auth-ui` | Login/Logout-Komponenten. |
| `@mana/shared-hono` | (sobald cards-server existiert) Auth-/Health-/Error-Middleware. |
| `@mana/shared-branding` | App-Registry-Eintrag (Tier=`public`, Branding, Subdomain). |
| `@mana/shared-types` | Geteilte TS-Typen. |
| `@mana/shared-utils` | Utility-Funktionen. |
| `@mana/shared-ui` | UI-Komponenten. |
| `@mana/shared-theme`, `@mana/shared-theme-ui` | Theme-Tokens, Dark/Light. |
| `@mana/shared-tailwind` | Tailwind-Preset. |
| `@mana/shared-i18n` | Übersetzungsfundament (Phase 1: nur DE registriert). |
| `@mana/shared-icons` | Icon-Set. |
| `@mana/shared-privacy` | Visibility-Enum für Decks (Sharing erst Phase 2, aber Feld vorbereitet). |
| `@mana/shared-crypto` | AES-GCM-256 für sensible Felder. |
| `@mana/shared-pwa` | Manifest, Service-Worker, Install-Prompt. |
| `@mana/shared-vite-config` | Vite-Defaults. |
| `@mana/shared-error-tracking` | Error-Reporting. |
| `@mana/shared-logger` | Strukturiertes Logging (Server-Seite, sobald relevant). |
| `@mana/shared-stores` | Geteilte Local-Store-Helpers. |
| `@mana/shared-tags` | Tags auf Decks. |
| `@mana/local-store` | Dexie-Setup, Sync-Hooks. |
| `@mana/eslint-config` | Lint-Regeln. |
| `@mana/test-config` | Vitest-Defaults. |
| `@mana/feedback` | In-App-Feedback-Widget. |
| `@mana/help` | Hilfe-Overlay. |
**Erst Phase 2 oder später:** `@mana/shared-llm`, `@mana/shared-ai`,
`@mana/local-llm`, `@mana/local-stt`, `@mana/credits`, `@mana/qr-export`,
`@mana/wallpaper-generator`, `@mana/website-blocks`,
`@mana/shared-research`, `@mana/shared-uload`, `@mana/shared-storage`.
### Datenpfad
Cardecky übernimmt 1:1 das Mana-Datenpfad-Pattern:
```
User-Aktion → Store → encryptRecord → Dexie → Hooks (_pendingChanges)
→ mana-sync → Postgres (mana_platform.cards.*) → andere Clients
```
appId = `cards`. Tabellen: `cardDecks`, `cards`, `cardReviews`,
`cardStudyBlocks`, `deckTags`.
## 6. Datenmodell — erweiterbar gedacht
Heutiges Modul kennt nur `front`/`back`. Damit weitere Kartentypen
ohne Schema-Bruch dazukommen, wechseln wir auf ein **Felder-Map +
Typ-Diskriminator**:
```ts
type CardType =
| 'basic' // Phase 1: front/back
| 'basic-reverse' // Phase 1: erzeugt zwei Lernrichtungen aus einer Karte
| 'cloze' // Phase 1: Lückentext, eine Subkarte pro Cluster
| 'type-in' // Phase 1: User tippt Antwort, exact-match-Vergleich
| 'image-occlusion' // Phase 2
| 'audio' // Phase 2
| 'multiple-choice' // ggf. Phase 2
interface LocalCard extends BaseRecord {
deckId: string
type: CardType
fields: Record<string, string> // basic: { front, back } · cloze: { text, extra? }
// FSRS-State liegt nicht hier, sondern in cardReviews (1:N pro Subkarte)
order: number
}
interface LocalCardReview extends BaseRecord {
cardId: string
subIndex: number // basic-reverse → 0|1, cloze → c1, c2, …
stability: number // FSRS
difficulty: number // FSRS
due: string // ISO
reps: number
lapses: number
state: 'new' | 'learning' | 'review' | 'relearning'
lastReview?: string
}
interface LocalCardStudyBlock extends BaseRecord {
date: string // YYYY-MM-DD
cardsReviewed: number
durationMs: number
}
```
**Cloze-Syntax:** Anki-kompatibel: `{{c1::Wort}}`, `{{c1::Wort::Hinweis}}`.
Eine Cloze-Karte mit Cluster `c1`+`c2` erzeugt 2 Reviews
(`subIndex 1`, `subIndex 2`).
**Markdown:** `marked` + `DOMPurify` rendern Front/Back. Cloze-Tags
werden vor dem Markdown-Parser zu HTML-Spans umgewandelt, damit sie im
Render erhalten bleiben.
**Migration aus dem Bestand:** existierende `front`/`back`-Karten werden
beim ersten Schema-Upgrade auf `type='basic'` mit
`fields={front, back}` migriert. Alte Spalten bleiben für eine
Übergangsversion lesbar (siehe `docs/DATABASE_MIGRATIONS.md`).
## 7. Daten-Contract mit dem mana-Modul
Wichtig: das **bestehende `cards`-Modul in der Mana-Web-App bleibt
erhalten**. Cardecky und das mana-Modul schreiben in dieselben
Postgres-Tabellen.
Daher gilt:
- Schema-Änderungen werden **gemeinsam** im mana-Modul und im
Cardecky-Code rolled out (nie nur auf einer Seite).
- Encryption-Registry-Einträge müssen in beiden Frontends identisch
sein (Field-Allowlist).
- Migrationen über `docs/DATABASE_MIGRATIONS.md`.
**Reihenfolge:** Phase 0 (mana-Modul um neue Tabellen + Kartentyp-Felder
+ FSRS erweitern) wird **vor** dem Standalone-Build durchgezogen. So
gibt es nie zwei Wahrheiten zur Datenstruktur.
## 8. Definition of Done für Phase 1
Phase 1 ist fertig, wenn:
1. Ein eingeloggter Mana-User kann auf `cardecky.mana.how`
- mindestens ein Deck anlegen,
- Karten manuell hinzufügen (Basic, Basic+Reverse, Cloze, Type-In),
- Markdown im Front/Back nutzen (Bold, Listen, Code, Links),
- eine Lernsession starten und mit FSRS-Bewertung durchspielen,
- die App schließen und am nächsten Tag die richtigen fälligen Karten wiederfinden.
2. FSRS-Per-User-Tuning läuft automatisch nach ≥ 50 Reviews und überschreibt die Default-Parameter.
3. Die App ist als PWA installierbar und offline-bedienbar (Karten lernen ohne Netz).
4. Auth läuft komplett über mana-auth (kein Eigen-Login).
5. Daten landen in Postgres und sind im bestehenden mana-Modul sichtbar (gleiche Datenquelle, kein Drift).
6. `pnpm validate:all` grün.
7. Mindestens drei Smoke-E2E-Tests (Playwright):
- „Login → Deck anlegen → Basic-Karte → Lernsession → bewerten"
- „Cloze-Karte mit zwei Clustern → erzeugt zwei Subkarten"
- „Type-In: korrekte Antwort = grün, falsche = rot"
8. Container baut & läuft auf dem Mac mini hinter Cloudflare Tunnel (`cardecky.mana.how`).
Alles andere ist Phase 2.
## 9. Repo-Struktur (Phase 1)
```
apps/cards/
├── apps/
│ └── web/ # SvelteKit-App, einziges Surface in Phase 1
│ ├── src/
│ │ ├── lib/
│ │ │ ├── data/ # Dexie + Sync-Anbindung
│ │ │ ├── fsrs/ # ts-fsrs-Wrapper + Optimizer-Hook
│ │ │ ├── cards/ # Kartentyp-Renderer (basic, cloze, type-in)
│ │ │ ├── stores/ # Decks, Cards, Reviews, StudyBlocks
│ │ │ └── ui/ # Komponenten (DeckList, CardEditor, Session)
│ │ └── routes/
│ │ ├── +layout.svelte
│ │ ├── +page.svelte # Heute fällig + Decks
│ │ ├── decks/[id]/+page.svelte # Deck-Detail + Karten
│ │ └── learn/[deckId]/+page.svelte # Lernsession
│ ├── package.json
│ ├── svelte.config.js
│ └── vite.config.ts
├── GUIDELINES.md # ← dieses Dokument
└── README.md
```
`apps/cards/apps/mobile/` und `apps/cards/apps/landing/` sind erst
Phase 2/3.
## 10. PR-Checkliste
Bei jedem Pull-Request gefragt:
- Gehört die Änderung zum Core Gameloop?
- Wenn nein: rechtfertigt sie sich aus einer Pflicht (Auth, Sync, Build)?
- Wird ein bestehendes `@mana/*` Paket genutzt statt neu zu bauen?
- Ist jede neue Dependency Open-Source und im Verein bereits in Verwendung?
- Sind Datenmodell-Änderungen mit dem mana-Modul konsistent?
- Bricht die Änderung das Versprechen "Erweiterbare Daten, simples UI"?
## 11. Analytics-Events (Mindestumfang Phase 1)
Über `mana-analytics`:
- `cards_session_started``{ deckId, dueCount }`
- `cards_card_rated``{ cardId, type, grade (14), elapsedMs }`
- `cards_session_completed``{ deckId, cardCount, durationMs }`
- `cards_deck_created``{ deckId }`
- `cards_card_created``{ deckId, type }`
- `cards_fsrs_optimized``{ reviewCount, paramsHash }`
- `cards_pwa_installed` — Standard-PWA-Event
Reicht für die Core-Loop-Validierung. Mehr Events erst, wenn eine
konkrete Frage entsteht, die Daten beantworten sollen.
## 12. Hinweis im mana-Modul
Sobald `cardecky.mana.how` live ist, bekommt das mana-Modul einen
**dezenten** Hinweis (z.B. ein Banner oder Badge über der ListView):
"Cardecky gibt es jetzt auch als eigenständige App". Kein Pop-up, kein
forcierter Redirect — User entscheiden selbst.

View file

@ -0,0 +1,654 @@
# Cardecky-Marktplatz — Plan
> **Status**: Plan, kein Code. Stand 2026-05-07.
> **Goal-Setting**: Vollvision, kein MVP-Druck. Wir bauen die optimale Lösung.
> **Alignment**: User hat folgende Eckpunkte gesetzt:
> - Versionierte Decks + Live-Updates + Pull-Requests = ja, volle Vision
> - mana-credits zentral, sowohl für User-Käufe als auch Author-Verdienst
> - „Verified" zweigleisig: Mana-Verein-Kuration UND Community-Schwellen, mit unterschiedlichen Badges
> - Co-Learn-Sessions explizit **nicht** für Phase 1 — auf Phase 2 verschoben
> - Mobile-App auch später
---
## 1. Mission
**Die Karteikarten-Plattform mit der besten Lern-Community im Netz.** Wo qualitativ hochwertige Decks entstehen, gepflegt, geteilt und gelernt werden — und wo Lernende einander helfen.
## 2. Was wir gegen die Konkurrenz aufbieten
(verdichtet aus `apps/cards/COMPETITORS_2026-05.md`)
| Differenzierer | Wir | Wer noch |
|---|---|---|
| Free Cloud-Sync | ✓ | niemand |
| Versionierte Decks mit Live-Updates | ✓ | nur AnkiHub (paywalled, Medizin-only) |
| Pull-Requests auf Decks | ✓ | niemand |
| Card-Discussions (inline pro Karte) | ✓ | niemand |
| AI-Karten + AI-Moderation + AI-Tags | ✓ | fragmentiert bei anderen |
| Open Source PWA | ✓ | nur Anki/Mnemosyne (Desktop) |
| Anki-Migration mit Bildern/Audio | ✓ (vorhanden) | niemand vollständig |
| Author-Followings + Activity-Feed | ✓ | niemand |
| Bezahlte Decks mit Author-Erlös via mana-credits | ✓ | nur Brainscape (eigenes Closed-Pricing) |
| Pseudonym + verifiziert kombinierbar | ✓ | niemand klar |
## 3. Architektur-Prinzipien
1. **API ist `/v1` ab Tag 1** — OpenAPI-Spec als Quelle der Wahrheit, Versionierungs-Bewusstsein eingebaut.
2. **Public-Decks leben separat** vom Local-First-Sync-Pfad (eigene Postgres-Tabellen, eigene Service, eigene RLS-Policies). Kein Vermischen mit `mana_sync.sync_changes`.
3. **Subscribed Decks sind unidirektional**: Author → Subscribers. Updates fließen einseitig. Wer ändern will, forkt.
4. **Content-Hash überall.** Jede Karte und jede Version bekommt einen deterministischen SHA-256 → Trust + Cache + Diff kostenlos.
5. **Lizenzen sind explizit + maschinen-lesbar** (SPDX-IDs: `CC0-1.0`, `CC-BY-4.0`, `CC-BY-SA-4.0`, plus eigener `Cardecky-Personal-Use-1.0` für Default-Käufe und `Cardecky-Pro-Only-1.0` für paid Decks).
6. **AI ist Moderator, nicht Gatekeeper** — KI-First-Pass + Human-Review-Eskalation. Niemals KI-allein-Take-down.
7. **Search ist von der DB entkoppelt** — Read-Only-Index, asynchron befüllt. Bricht der Search-Service, läuft der Marktplatz weiter.
8. **mana-credits ist die einzige Geld-Schnittstelle** — niemals Stripe direkt im cards-server. Alles geht über `/api/v1/credits/use`, `/credits/grant`, `/credits/reservations/*`.
9. **Anonymisiertes Lern-Verhalten**: aggregierte Stats sichtbar (z.B. „1.200 Lernende"), individuelles Lernverhalten nie öffentlich ohne explizites Opt-in.
10. **Keine Drittanbieter-Tracker.** Telemetrie ausschließlich über mana-analytics, opt-out möglich.
## 4. Datenmodell
Neues Schema `cards` in `mana_platform`. Alle Tabellen über `pgSchema('cards').table(...)` (Mana-Konvention).
### 4.1 Authoren
```sql
public_authors (
user_id uuid PRIMARY KEY REFERENCES auth.users(id),
slug text UNIQUE NOT NULL, -- @anna-lang
display_name text NOT NULL,
bio text,
avatar_url text,
joined_at timestamptz DEFAULT now(),
pseudonym boolean DEFAULT false, -- true = klarname versteckt
verified_mana boolean DEFAULT false, -- vom Verein verliehen
verified_community boolean DEFAULT false, -- automatisch ab Schwelle
banned_at timestamptz, -- soft-ban
banned_reason text
)
```
Drei Verifizierungs-Stufen mit unterschiedlichen Badges in der UI:
| Status | Badge | Wer / wie |
|---|---|---|
| `verified_mana = true` | 🛡️ **Mana Verifiziert** | Manuell vom Mana-Verein vergeben (Lehrer, Profis, Sprachschulen, Ärzte). Nicht erkaufbar. |
| `verified_community = true` | ⭐ **Community Verifiziert** | Automatisch bei: ≥ 500 Stars über alle Decks ODER ≥ 3 featured Decks ODER ≥ 200 aktive Subscribers über alle Decks. Periodisch neu evaluiert. |
| beides | 🛡️⭐ Beide Badges | Mana + Community zusammen. |
### 4.2 Decks + Versionen
```sql
public_decks (
id uuid PRIMARY KEY,
slug text UNIQUE NOT NULL, -- /decks/anna-lang/spanish-a2-vocab
title text NOT NULL,
description text,
language text, -- ISO-639-1
license text NOT NULL, -- SPDX
price_credits integer DEFAULT 0, -- 0 = kostenlos
owner_user_id uuid NOT NULL REFERENCES public_authors(user_id),
latest_version_id uuid, -- → public_deck_versions
is_featured boolean DEFAULT false,
is_takedown boolean DEFAULT false,
takedown_at timestamptz,
takedown_reason text,
created_at timestamptz DEFAULT now(),
CONSTRAINT price_requires_license CHECK (price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0')
)
public_deck_versions (
id uuid PRIMARY KEY,
deck_id uuid NOT NULL REFERENCES public_decks(id),
semver text NOT NULL, -- 1.0.0, 1.1.0, 2.0.0
changelog text,
content_hash text NOT NULL, -- SHA-256 of canonicalized cards
card_count integer NOT NULL,
published_at timestamptz DEFAULT now(),
deprecated_at timestamptz,
UNIQUE (deck_id, semver)
)
public_deck_cards (
id uuid PRIMARY KEY,
version_id uuid NOT NULL REFERENCES public_deck_versions(id),
type text NOT NULL, -- basic, basic-reverse, cloze, type-in
fields jsonb NOT NULL, -- {front, back} oder {text, extra}
ord integer NOT NULL,
content_hash text NOT NULL, -- per Karte: ermöglicht Smart-Merge
UNIQUE (version_id, ord)
)
```
### 4.3 Tags + Discovery
```sql
tag_definitions (
id uuid PRIMARY KEY,
slug text UNIQUE NOT NULL,
name text NOT NULL,
parent_id uuid REFERENCES tag_definitions(id), -- Hierarchie
description text,
curated boolean DEFAULT false -- vom Mana-Verein gepflegt
)
deck_tags (
deck_id uuid REFERENCES public_decks(id),
tag_id uuid REFERENCES tag_definitions(id),
PRIMARY KEY (deck_id, tag_id)
)
```
### 4.4 Engagement (Stars, Subscribes, Forks)
```sql
deck_stars (
user_id uuid REFERENCES auth.users(id),
deck_id uuid REFERENCES public_decks(id),
starred_at timestamptz DEFAULT now(),
PRIMARY KEY (user_id, deck_id)
)
deck_subscriptions (
user_id uuid REFERENCES auth.users(id),
deck_id uuid REFERENCES public_decks(id),
current_version_id uuid REFERENCES public_deck_versions(id),
subscribed_at timestamptz DEFAULT now(),
notify_updates boolean DEFAULT true,
PRIMARY KEY (user_id, deck_id)
)
deck_forks (
user_id uuid REFERENCES auth.users(id),
source_deck_id uuid REFERENCES public_decks(id),
source_version_id uuid REFERENCES public_deck_versions(id),
forked_at timestamptz DEFAULT now(),
PRIMARY KEY (user_id, source_deck_id, source_version_id)
)
author_follows (
follower_user_id uuid REFERENCES auth.users(id),
author_user_id uuid REFERENCES public_authors(user_id),
since timestamptz DEFAULT now(),
PRIMARY KEY (follower_user_id, author_user_id)
)
```
### 4.5 Pull-Requests + Discussions
```sql
deck_pull_requests (
id uuid PRIMARY KEY,
deck_id uuid REFERENCES public_decks(id),
author_user_id uuid REFERENCES auth.users(id),
status text NOT NULL, -- open, merged, closed, rejected
title text NOT NULL,
body text,
diff jsonb NOT NULL, -- {add: [...], modify: [...], remove: [...]}
merged_into_version uuid REFERENCES public_deck_versions(id),
created_at timestamptz DEFAULT now(),
resolved_at timestamptz
)
card_discussions (
id uuid PRIMARY KEY,
card_content_hash text NOT NULL, -- bindet sich an Karte, nicht an version
deck_id uuid REFERENCES public_decks(id),
author_user_id uuid REFERENCES auth.users(id),
parent_id uuid REFERENCES card_discussions(id),
body text NOT NULL,
hidden boolean DEFAULT false,
created_at timestamptz DEFAULT now()
)
```
### 4.6 Moderation
```sql
deck_reports (
id uuid PRIMARY KEY,
deck_id uuid REFERENCES public_decks(id),
version_id uuid REFERENCES public_deck_versions(id),
card_content_hash text, -- optional: Karte spezifisch
reporter_user_id uuid REFERENCES auth.users(id),
category text NOT NULL, -- spam, copyright, nsfw, misinformation, other
body text,
status text DEFAULT 'open', -- open, dismissed, actioned
resolved_by uuid,
resolved_at timestamptz,
resolution_notes text,
created_at timestamptz DEFAULT now()
)
ai_moderation_log (
id uuid PRIMARY KEY,
version_id uuid REFERENCES public_deck_versions(id),
verdict text NOT NULL, -- pass, flag, block
categories text[], -- spam, csam, hate, nsfw, ...
model text, -- "claude-3-5-sonnet" etc
rationale text,
human_reviewed boolean DEFAULT false,
human_overrode boolean DEFAULT false,
created_at timestamptz DEFAULT now()
)
```
### 4.7 mana-credits Integration
```sql
deck_purchases (
id uuid PRIMARY KEY,
buyer_user_id uuid REFERENCES auth.users(id),
deck_id uuid REFERENCES public_decks(id),
version_id uuid REFERENCES public_deck_versions(id),
price_credits integer NOT NULL, -- Snapshot zum Zeitpunkt des Kaufs
author_share integer NOT NULL, -- nach Verein-Cut
mana_share integer NOT NULL,
credits_transaction text, -- mana-credits ID
purchased_at timestamptz DEFAULT now(),
refunded_at timestamptz,
UNIQUE (buyer_user_id, deck_id) -- einmal Kauf reicht für Lifetime + alle Versionen
)
author_payouts (
id uuid PRIMARY KEY,
author_user_id uuid REFERENCES public_authors(user_id),
source_purchase_id uuid REFERENCES deck_purchases(id),
credits_granted integer NOT NULL,
credits_grant_id text, -- mana-credits grant ID
granted_at timestamptz DEFAULT now()
)
```
## 5. mana-credits Integration (Detail)
Zwei-seitiger Marktplatz. mana-credits ist Single-Source-of-Truth fürs Geld.
### 5.1 Kauf-Flow (Buyer)
1. User klickt „Kaufen" auf paid Deck (Preis: z.B. 50 Credits)
2. cards-server checkt: Hat User schon dieses Deck? (deck_purchases) → wenn ja, sofort Zugriff
3. cards-server reserviert Credits via `POST mana-credits/api/v1/credits/reservations` (2-phase)
4. cards-server erstellt deck_purchases-Row (committed)
5. cards-server commit-released die Reservation → Credits abgebucht
6. cards-server erstellt author_payouts-Row → ruft `POST mana-credits/api/v1/internal/credits/grant` für den Author-Anteil
7. User bekommt sofortigen Zugriff: Deck wird in private Liste verschoben (User hat eine eigene Lokal-Kopie als Author-Subscription)
**Was passiert wenn Author gebannt nach Kauf?** → Refund-Path (Phase γ Implementation): Admin kann Refund triggern → mana-credits → Reverse-Grant → User behält das Deck nicht mehr.
### 5.2 Author-Auszahlungs-Modell
- **Standard-Cut**: 80 % Author / 20 % Mana-Verein (Server-, Hosting-, Moderations-Kosten)
- **Verifizierte Authoren** (verified_mana): 90 % / 10 %
- **Mindestauszahlung**: keine — Credits werden direkt im mana-credits-Account gebucht, von dort kann der Author sie selbst nutzen oder per Stripe-Payout (mana-credits-Feature, falls vorhanden) abheben
- **Pricing-Range**: Free (0 Credits), oder 10500 Credits (entspricht ungefähr 150 € — exakte Conversion siehe mana-credits packages)
### 5.3 Käufer-Lebenszyklus
- Einmal gekauft = Lifetime-Zugriff auf alle künftigen Versionen
- Bei major Version (e.g. 1.x → 2.0.0) **kein** zweiter Kauf nötig — Author behält die Verbesserungs-Pflicht
- Refund-Window: 30 Tage, automatisch verfügbar wenn ≤ 10 % der Karten gelernt wurden (Quizlet hat das, ist Best-Practice)
### 5.4 Buyer-Protection bei Take-Down
- Wenn Deck per Take-Down entfernt wird, behält Buyer Zugriff auf das letzte gesehene Snapshot (DSGVO-konform)
- Refund automatisch wenn Take-Down innerhalb 90 Tagen nach Kauf
## 6. Service-Architektur
### 6.1 `cards-server` (neu)
- **Stack**: Hono + Bun (Mana-Konvention)
- **Port**: 3072
- **Deps**: PostgreSQL (`mana_platform.cards.*`), Redis (Job-Queue für Indexing/Notifications)
- **Auth**: JWT via JWKS (mana-auth)
- **Routes**: siehe §7
### 6.2 `cards-search` (neu, später)
- Eigene PostgreSQL-Instance mit pg_trgm + tsvector + pgvector
- Async-Indexer hört auf cards-server-Events („deck-published", „deck-updated")
- Optional: Meilisearch wenn Postgres FTS nicht reicht
### 6.3 mana-llm (existierend, erweitert)
- Embeddings für semantic search (jeden Deck-Description + Karte → 1536-dim Vector)
- Moderation-First-Pass (Klassifikation in spam/csam/hate/nsfw/etc.)
- Auto-Tag-Suggestions
- Auto-Summary für Deck-Beschreibungen
### 6.4 mana-credits (existierend, erweitert)
- Bestehende `/credits/use` und `/credits/reservations/*` für Kauf
- Bestehender `/internal/credits/grant` für Author-Auszahlung
- Vermutlich keine API-Erweiterung nötig
### 6.5 mana-notify (existierend, erweitert)
- Push-Notifications für Subscribe-Updates, neue Subscribers, neue Discussions/Replies, neue Stars (vom User konfigurierbar)
### 6.6 mana-media (existierend)
- Bilder/Audio in published Decks landen wie heute auch
- Pro Author-Tier ein Soft-Quota: Free 100MB, Verified 1GB, Mana 5GB
## 7. API-Endpoints (Auswahl)
OpenAPI-Spec wird die Quelle der Wahrheit; hier die wichtigsten Routes:
### 7.1 Authoren
```
POST /v1/authors/me — Profil anlegen/updaten (slug, displayName, bio, avatar, pseudonym)
GET /v1/authors/:slug — Public Profile + Decks-Liste + Stats
GET /v1/authors/me/dashboard — Eigene Stats: Subscriber, Erlöse, Mod-Inbox
POST /v1/authors/:slug/follow — Folgen
DELETE /v1/authors/:slug/follow — Entfolgen
GET /v1/authors/me/feed — Personal Activity-Feed
```
### 7.2 Decks
```
POST /v1/decks — Deck als public registrieren (Init-Flow)
GET /v1/decks/:slug — Public Deck mit latest version
GET /v1/decks/:slug/versions — Versionsliste mit Changelogs
GET /v1/decks/:slug/versions/:semver — Specific Version + alle Karten
PATCH /v1/decks/:slug — Metadaten (title, description, license, price)
POST /v1/decks/:slug/publish — Neue Version publishen (body: cards[], semver, changelog)
→ triggert AI-Mod-Pass
→ setzt latest_version_id
POST /v1/decks/:slug/star — Star setzen
DELETE /v1/decks/:slug/star — Star entfernen
POST /v1/decks/:slug/subscribe — Subscribe (lädt + sync'd Karten in lokale DB)
DELETE /v1/decks/:slug/subscribe — Unsubscribe
POST /v1/decks/:slug/fork — Fork (lokale Kopie + Author-Lineage)
POST /v1/decks/:slug/buy — Paid Deck kaufen (mana-credits-Flow)
POST /v1/decks/:slug/refund — Refund anfragen
```
### 7.3 Pull-Requests
```
GET /v1/decks/:slug/pull-requests — Liste
POST /v1/decks/:slug/pull-requests — Neuer PR (body: title, body, diff)
GET /v1/pull-requests/:id — Details
POST /v1/pull-requests/:id/merge — Author merged → erstellt neue Version
POST /v1/pull-requests/:id/close — Author schließt
POST /v1/pull-requests/:id/comments — Diskussion auf PR-Ebene
```
### 7.4 Discussions
```
GET /v1/cards/:contentHash/discussions — Threads für eine Karte (über Versionen hinweg)
POST /v1/cards/:contentHash/discussions — Neuer Thread / Reply
POST /v1/discussions/:id/hide — Author/Mod versteckt
```
### 7.5 Discovery + Search
```
GET /v1/explore — Featured + Trending + Categories (curated)
GET /v1/search?q=…&tag=…&lang=…&sort=… — Volltextsuche (FTS + semantic)
GET /v1/tags — Tag-Hierarchie
GET /v1/decks?author=…&tag=…&sort=…&p=… — Filtered Browse
```
### 7.6 Reports + Moderation
```
POST /v1/decks/:slug/report — User reportet Deck
POST /v1/cards/:contentHash/report — User reportet Karte
GET /v1/admin/reports — Admin-Inbox (verifizierte Mana-Mods only)
POST /v1/admin/decks/:slug/takedown — Admin entfernt Deck
POST /v1/admin/authors/:slug/ban — Admin sperrt Author
POST /v1/admin/authors/:slug/verify-mana — Mana-Verein-Badge vergeben
```
### 7.7 Notifications
```
GET /v1/notifications — Unread + recent
POST /v1/notifications/:id/read — Mark read
PATCH /v1/notifications/preferences — Settings (welche Events triggern Push)
```
## 8. UI / Routes (Cardecky-Frontend)
```
/explore — Featured + Trending + Tag-Tree + Search-Bar
/explore/search?q=… — Search-Result-Page
/explore/tag/:slug — Tag-Page
/u/:slug — Author-Profil (Public)
/u/:slug/follow — Follow-Button im Header
/d/:slug — Public-Deck-Detail-View
(Description, Stats, Latest-Karten-Preview, Subscribe/Fork/Star/Buy, Discussions)
/d/:slug/v/:semver — spezifische Version
/d/:slug/discussions — Alle Discussions zum Deck
/d/:slug/pull-requests — PRs
/d/:slug/pull-requests/:id — PR-Detail mit Diff-View
/me/decks — Eigene private Decks (heute existiert)
/me/published — Eigene published Decks + Stats
/me/subscribed — Abonnierte Decks (mit Update-Indikator)
/me/forks — Geforkte Decks
/me/dashboard — Author-Dashboard (Erlöse, Subscriber-Wachstum)
/feed — Personal Activity-Feed (Following-Activity + Updates)
/admin/reports — Admin-Inbox (verified-mana-only)
/admin/decks — Take-Down-UI
/admin/authors — Verify + Ban
```
Zusätzlich: einige bestehende Komponenten erweitern (DeckDetail bekommt Subscribe-Button etc.).
## 9. Cold-Start-Strategie
Marktplatz ohne Decks ist nutzlos. Drei parallele Hebel:
1. **Verein-Seed-Decks**: 50 hochwertige Decks selbst erstellen — sprachen (Top-3000 Vokabeln pro Sprache), Geschichte (TimeLine-Karten), Allgemeinwissen, Programmierung. Vom Mana-Team published, alle mit `verified_mana`-Badge.
2. **Anki-Top-100-Import-Service**: Wir bieten an, populäre Anki-Web-Decks (mit korrekter CC-BY-Lizenz) zu importieren und mit Original-Author-Attribution als Public-Decks anzulegen. Original-Author bekommt das `verified_mana`-Badge wenn er sich registriert.
3. **Influencer-Outreach**: Direkte Ansprache von 10-20 Anki-Power-Authoren (AnKing, etc.) mit dem Angebot eines verified-Status + sehr Author-freundlichem Cut. Wenn 1-2 wechseln, kommt ein Lawineneffekt.
## 10. Risiken + Mitigationen
| Risiko | Mitigation |
|---|---|
| Cold-Start (Marktplatz leer) | Seed + Anki-Import + Influencer (siehe §9) |
| Spam / Junk-Decks | AI-Mod-First-Pass + Report-System + Author-Ban-Flow |
| Copyright-Klagen (Lehrbuch-Karten) | Lizenz-Pflichtangabe + DMCA-Process + Take-Down-Workflow |
| Server-Kosten (Storage von Bildern/Audio) | Soft-Quotas pro Author-Tier (§6.6) + lossy compression im mana-media |
| AnkiHub als Konkurrent (Live-Updates Medizin) | „Alle Fachgebiete + gratis" als Counter; Med-Decks aktiv akquirieren |
| Mana-Credits-Verein-Cut zu hoch oder zu niedrig | A/B-Test verschiedener Cut-Verhältnisse; Best-Practice: ~80/20 für Standard, ~90/10 für Verified |
| Author-Frustration über fehlende Mobile-App | Klarer Roadmap-Hinweis + Mobile-Push-Notifications via PWA (heute geht das schon) |
| Discussions werden Toxic | Author-Owns-Their-Discussions (kann hide); Community-Mod (Verified-User können flaggen); klar dokumentierte Community-Guidelines |
| Mining/Scraping der Decks | Rate-limit auf API + Auth-Required für full-content; offene Snippets aber paywall am Voll-Inhalt |
## 11. Phasenplan
> **Co-Learn explizit ausgeklammert.** Mobile-App auch.
### Phase α — Daten-Skelett (cards-server v0.1)
- `services/cards-server/` SvelteKit-style Service-Setup, Hono + Bun + Drizzle
- Alle Schema-Tabellen + Migrationen (§4)
- API-Routes (CRUD-Niveau): Authoren, Decks, Versionen, Stars, Subscriptions
- OpenAPI-Spec
- Integration-Tests (Drizzle + Vitest)
- mana-auth-JWT-Middleware (`@mana/shared-hono`)
- Container in `docker-compose.macmini.yml`
- Cloudflare-Tunnel-Route `cardecky-api.mana.how``:3072`
### Phase β — Author-Workflow ✅ shipped
- ✅ „Author werden"-Flow im Frontend (Profil anlegen, slug claimen)
- ✅ „Publish"-Aktion auf Deck-Detail-Seite
- ✅ Lizenz-Picker (SPDX-Auswahl)
- ✅ Optional: Preis in Credits
- ⏳ Tags: Picker fehlt im Publish-Flow; Server-Schema steht
- ✅ Versioning: semver-Eingabe (Auto-Suggest pre-fill folgt in θ)
- ✅ Changelog-Editor
- ✅ AI-First-Pass-Moderation (mana-llm classify, Verdict im Publish-Result)
- ⏳ Author-Dashboard mit Subscriber-Counts: Erlöse jetzt unter `/me/purchases`, restliche Stats fehlen
### Phase γ — Discovery-Frontend ✅ shipped (FTS minimal)
- ✅ `/explore`-Seite mit Featured + Trending
- 🟡 Volltext-Suche: einfaches `ILIKE` über Title/Description; tsvector-Upgrade in Phase ι
- 🟡 Tag-Hierarchie: flach implementiert; baumartige Eltern-Kind-Navigation offen
- ✅ Author-Profile (`/u/<slug>`) + Follow-Button
- ⏳ Activity-Feed (wer hat was published / merged): nicht gebaut
- ✅ Star-System
### Phase δ — Subscribe + Updates + Smart-Merge ✅ shipped
- ✅ „Abonnieren"-Button → lädt aktuelle Version in lokale Cardecky-DB
- 🟡 Update-Detection: Polling beim Öffnen der Deck-Page; **kein** WebSocket-Push (kommt in θ/ι)
- ✅ **Smart-Merge**: Diff zwischen Versionen → unveränderte Karten behalten FSRS-State; geänderte erben FSRS-State über Ord-Pairing-Heuristik; neue + entfernte werden korrekt behandelt
- ✅ Diff-View „+N · ~N · N" mit Apply-Button auf der Deck-Page
- ⏳ Push-Notifications für Subscribe-Updates via mana-notify: PR-/Verkaufs-Mails sind drin (ε.3, ζ.1), Update-Mail noch nicht
### Phase ε — Pull-Requests + Discussions ✅ shipped
- ✅ PR-Erstellen-UI: „✏️ Verbessern" auf `/learn/[id]` für Karten aus abonnierten Decks (modify oder remove)
- ✅ PR-Diff-Preview (flach, alle drei Blöcke `add` / `modify` / `remove`)
- ✅ Author-Merge-Workflow → erstellt neue Version atomar, bumped semver-Minor by default
- ✅ Inline-Discussion-Threads: in `/learn` (Toggle) + auf `/d/<slug>` (Karten-Liste mit Comment-Counts)
- ✅ Notify: Author bei neuem PR; PR-Author bei Merge/Reject (deterministische ExternalIDs für Dedup)
- ⏳ Mention-System (@username): nicht gebaut; Schema-Änderung später trivial
- 🟡 PR-Merge ist „stale-blind": kein Rebase / Konflikt-Detection (siehe §13a)
### Phase ζ — mana-credits Marketplace 🟡 ζ.1 shipped, ζ.2 offen
- ✅ Paid-Deck-Workflow End-to-End: 4-step Pipeline `reserve → INSERT purchase → commit → grant author + INSERT payout`, idempotent über `(buyer, deck)`
- ✅ Author-Auszahlungs-Pipeline: 80/20 Standard, 90/10 für `verifiedMana`-Authoren, kommt aus `config.authorPayout` (Basis-Punkte)
- ✅ Buyer-Dashboard `/me/purchases` mit Käufen + Author-Auszahlungs-Historie
- ⏳ **Refund-Workflow**: bewusst out-of-scope für ζ.1 (Author-Clawback ist konzeptuell heikel — siehe §13a)
- ⏳ **Reconciler**: bei Commit-/Grant-Failure nach Schritt 2 bleibt eine Purchase-Row mit `creditsTransaction = null` bzw. ohne Payout. Code logged, niemand fegt nach. Cron-Sweep in ζ.2
- ⏳ Author-Payouts-CSV-Export für Steuern
### Phase η — Moderation + Trust 🟡 η.1 shipped, η.2/η.3 offen
- ✅ Report-Buttons auf Deck (`/d/<slug>`) + Discussion-Kommentare
- ✅ Admin-Inbox-UI (`/admin/reports`) mit Abweisen / Deck-Takedown / Author-Bann
- ✅ Take-Down-Workflow: transaktional, auto-closed parallele Reports + offene PRs auf demselben Deck, Mail an Author
- 🟡 Verified-Badge-Vergabe via API (`POST /v1/admin/authors/:slug/verify`); kein dediziertes UI
- ⏳ **Community-Verified Auto-Calculation**: Schema + Schwellwerte da; Cron-Job fehlt (η.2)
- ⏳ **Public Take-Down-Changelog**: Plan erwähnt das, nicht gebaut
- ⏳ **Verified-Mana-only Mods**: aktuell nur `role === 'admin'`; Plan-Vision ist „verified-mana darf auch resolven" — feiner Cut, später
- ⏳ Author-Ban-Process: Ban kaskadiert auf Decks ✅, aber kein Self-Service-Appeal-Flow für Author
- ⏳ Report-Spam-Schutz (Rate-Limit pro User+Deck): nicht da
### Phase θ — Deep AI
- Auto-Tag-Suggestions beim Publish (mana-llm)
- Auto-Summary für Decks (mana-llm Markdown-Render-tauglich)
- Audio-Vertonung mit mana-tts (Author opt-in: alle Karten als Audio generieren)
- Semantic-Search via Embeddings (mana-llm + pgvector)
- Personalized-Discovery („Empfohlen für dich" basierend auf Lern-Historie)
### Phase ι — Optimierung + Skalierung
- Search-Service als separater Pod (Meilisearch wenn Postgres FTS limitiert)
- CDN für public-deck-content (Cache + Geo-Distribution)
- Rate-Limiting + Anti-Scraping
- Real-time-Stats-Aggregation (Materialized Views)
### Phasen die später kommen (explizit nicht in diesem Plan)
- **Phase λ — Co-Learn-Sessions**: WebSocket-Multiplayer, gemeinsam lernen, Sehen-was-andere-machen
- **Phase μ — Mobile-Apps**: Expo-App (Cardecky-Standalone-Mobile)
- **Phase ν — Author-Tools**: Bulk-Edit-UI für Authoren mit großen Decks, Style-Templates, Author-Analytics-Deep-Dive
- **Phase ξ — Lern-Battles**: Asynchroner Wettkampf-Modus
## 12. Konkrete Differenzierungs-Hebel — was geht wirklich nur bei uns
1. **Gratis Cloud-Sync + Live-Updates auf abonnierte Decks**. Niemand sonst hat beides ohne Paywall.
2. **Pull-Requests auf Decks**. AnkiHub erlaubt das nicht so flüssig, andere gar nicht. „Lerne und verbessere mit" als Modus.
3. **Card-Discussions inline** — wenn ich beim Lernen eine Karte unverständlich finde, kann ich direkt fragen / ergänzen. Anki hat Plugin dafür, RemNote auch nicht.
4. **Authoren verdienen via mana-credits** — wir behandeln Authoren als 1st-Class-Konstrukt mit Erlös-Möglichkeit. Quizlet macht das nicht, AnkiWeb macht das nicht, Brainscape paywalled stattdessen die User.
5. **Open Source PWA** mit klarer Roadmap-Transparenz — Vertrauensvorsprung vs. Quizlet (closed, Trustpilot 1.4/5) und gegenüber AnkiPro/AnkiApp (closed-source, Brand-Sniper).
6. **Doppelte Verifizierungs-Stufen** mit unterschiedlichen Badges — Anki-Foren machen das ad-hoc; wir formalisieren es.
7. **AI als Moderator + Generator + Indexer** ohne Paywall — wir haben den eigenen mana-llm-Stack, Konkurrenten zahlen OpenAI per Call.
## 13. Was wir NICHT tun
- **Kein Decks-Bewertungssystem mit 1-5 Sternen**. Stars (Bookmarks) ja, Bewertungen nein — die werden gegamed (Quizlet-Erfahrung), und führen zu Author-Frust + Review-Bombing.
- **Kein Reddit-Style-Voting auf Karten / PRs / Discussions**. Wirkt cool, ruiniert die Community (Hacker-News-Effekt). Lieber „helpful"-Reactions in begrenzten Kategorien.
- **Kein „Karten der Woche" allein-algorithmisch**. Editorial-Pick (Mana-Verein) + Trending-Liste, aber niemals nur Algo, das landet immer beim niederschwelligsten Content.
- **Kein Anki-Bashing im Marketing**. Anki ist OSS, ehrlich, und wir wollen nicht ihre Audience entfremden — wir wollen sie ergänzen. Bridge nicht Burning.
- **Keine Pflicht-Klarnamen**. Pseudonyme bleiben gleichberechtigt. Verifizierung ist Bonus, nicht Pflicht.
- **Kein Marketplace-Cut über 30 %**. Apple-App-Store-Hass ist real, wir bleiben fair.
## 13a. Bekannte Limitierungen / „macht später"
**Phase ε (Pull-Requests + Discussions)**
- **PR-Merge ist stale-blind**: `merge()` baut die neue Version aus `currentCards` zusammen, indem es Removes anwendet, dann Modifies-by-Hash, dann Adds. Wenn der Author zwischen PR-Open und Merge selbst eine Karte geändert hat, deren `previousContentHash` der PR matched, gewinnt **stumm** der PR — kein Konflikt-Hinweis. Akzeptabel solange wir wenige PRs/Tag haben; später entweder (a) PR-rebase mit `status=stale` bei Konflikt, oder (b) optimistic locking via `baseVersionId` auf der PR-Row mit Reject bei Mismatch.
- **Keine Multi-Card-Diff-Visualisierung**: PR-Diff-Preview zeigt jeden Block (`add` / `modify` / `remove`) flach. Bei großen PRs mit 50+ Karten unübersichtlich — Side-by-side-Vergleich pro modify wäre nett.
- **Discussion-Threading ist 1-Level**: Server speichert schon `parent_id`, aber das UI rendert flach. Bei Bedarf später ein Antworten-Button + visuelle Einrückung — kein Schema-Change nötig.
- **Card-Preview-Heuristik ist roh**: `<DeckCardList>` zieht `front``text` → erstes nicht-leeres Feld, strippt HTML, capt bei 140 Zeichen. Bei Cloze-Karten sieht der Leser den Roh-Text mit `{{c1::…}}`-Markern statt der maskierten Lern-Form. Kein Showstopper; später kann der Server eine `searchPreview`-Spalte schreiben.
**Phase ζ (Paid Decks)**
- **Refunds**: bewusst weggelassen. Author-Clawback ist konzeptuell heikel, weil der Author seinen Anteil nach Grant schon ausgegeben haben kann (→ 402 beim Reverse-Charge). Empfohlene ζ.2-Variante: Admin-only Refund, Buyer kriegt vollen Preis zurück, Author-Clawback nur best-effort, AGB-Klausel über Author-Cut-Risiko bei Refund.
- **Reconciler fehlt**: Wenn `commit` oder `grant` nach Schritt 2 fehlschlägt, bleibt eine Purchase-Row mit `creditsTransaction = null` bzw. ohne `author_payout`. Code logged das, aber niemand fegt nach. Cron-Sweep in ζ.2.
- **Buyer hat keinen Refund-Self-Service**: kein 30-Tage-Window-Knopf in der UI. Plan §5.3 sieht ihn vor; warten auf ζ.2.
- **CSV-Export für Steuern**: nicht drin. Easy add-on, sobald Verein die Steuerklärung 2026 vorbereitet.
**Phase η (Moderation)**
- **Verified-Mana-only Mods**: Admin-Gate ist aktuell `role === 'admin'`. Plan §11 sieht vor, dass auch verified-mana-Authoren Reports abarbeiten dürfen (mit eingeschränkten Aktionen). Würde nach den ersten 50 Reports sinnvoll, vorher over-engineered.
- **Community-Verified Cron**: Schema + Schwellwerte (`COMMUNITY_VERIFY_STARS=500`, `_FEATURED=3`, `_SUBSCRIBERS=200`) sind im config, aber kein Job berechnet `verified_community`. Add-on: ein Cron-Endpoint im internal API + SystemD-Timer auf Mac mini.
- **Public Take-Down-Changelog**: Plan erwähnt eine `/transparency`-Page — nicht gebaut. Bringt Trust, niedrige Priorität.
- **Appeal-Self-Service**: Author hat keinen Self-Service-Knopf für Restore. Bewusste Entscheidung — Appeals sollen menschlich sein, kein Self-Restore.
- **Report-Spam-Schutz**: ein User kann unbegrenzt Reports gegen ein Deck filen. Rate-Limit (max 1/User+Deck+Tag) wäre billig; kommt mit Phase ι.
**Querschnittsthemen**
- **Disk-Space auf der Build-Maschine** (Mac mini): aktuell ~6.7 GB frei. `pnpm store prune` als nächste Notbremse, falls cards-web-Builds enge Container-Layer brauchen.
## 14. Offene Punkte die später entschieden werden müssen
- **Mobile-Push-Notifications** für Subscribe-Updates: native PWA-Push reicht aktuell, aber Browser-API ist hin- und her — könnte Phase ι in einen eigenen Push-Service auslagern müssen.
- **Slack/Discord-Bots für Author-Updates**: nice-to-have, irgendwann.
- **Embed-Widget**: „Lerne dieses Deck auf meiner Webseite" mit IFrame — könnte Reichweite stark boosten.
- **API-Public**: API-Keys für Drittentwickler die eigene Tools rund um Cards bauen.
- **Backup für Subscriber**: Wenn ein Author published-Deck depubliziert, behalten Subscriber das letzte Snapshot (DSGVO-pflicht eh).
- **Internationalisierung der UI** (heute nur DE): nötig fürs internationale Publikum.
## 15. Aktueller Stand 2026-05-07
| Phase | Status | Was läuft | Was fehlt |
|-------|--------|-----------|-----------|
| α — Skelett | ✅ | cards-server lebt auf 3072, Schema gepushed, JWT-Auth, Container in `docker-compose.macmini.yml`, Tunnel-Route `cardecky-api.mana.how` | — |
| β — Author-Workflow | ✅ | Profil-Claim, Publish, Lizenz, Preis, AI-Mod-Verdict | Tag-Picker im Publish, Author-Dashboard-Stats |
| γ — Discovery | ✅ | `/explore`, Stars, Follows, Author-Profile, Trending | tsvector-FTS, Tag-Tree, Activity-Feed |
| δ — Subscribe + Smart-Merge | ✅ | Pull, Smart-Merge mit FSRS-State-Erhalt, Diff-View | WebSocket-Push, Update-Mails |
| ε — PRs + Discussions | ✅ | PR-Erstellen / List / Merge / Reject / Close, Discussions auf `/learn` + `/d/<slug>`, Notify-Mails | Mention-System, PR-Rebase, Multi-Card-Diff-View, Discussion-Threading |
| ζ — Paid Decks | 🟡 ζ.1 | Buy-Flow, Author-Payout, Buyer-Dashboard | Refund, Reconciler, CSV-Export |
| η — Moderation | 🟡 η.1 | Reports, Admin-Inbox, Takedown, Ban-Cascade, Verify-API | Community-Verified-Cron, Public-Changelog, Verified-Mana-Mod-Permissions, Rate-Limit |
| θ — Deep AI | ⏳ | — | Auto-Tags, Auto-Summary, TTS, Embeddings, Personalized-Discovery |
| ι — Optimierung | ⏳ | — | Search-Service, CDN, Rate-Limiting, Materialized Views |
| λ / μ / ν / ξ | ⏳ | — | später (Co-Learn, Mobile, Author-Tools, Lern-Battles) |
**Live-Domains**: `cardecky.mana.how` (Web) · `cardecky-api.mana.how` (API).
**Nächste sinnvolle Schritte (Empfehlung)**:
1. **ζ.2 Reconciler + minimaler Admin-Refund** — schließt das größte operative Loch im Paid-Flow.
2. **η.2 Community-Verified-Cron** — Plan-Vision der „doppelten Verifizierung" ist sonst nur halb umgesetzt; Cron ist klein.
3. **Update-Mail in δ.4** — Subscriber bekommen sonst nichts mit, wenn Author published. Dann ist die Notify-Story rund (PR-Open + PR-Merged + PR-Rejected + Verkauf + Takedown + Update).
4. **Phase θ starten** — Auto-Tags + Auto-Summary beim Publish via mana-llm: kostet wenig Code, viel Discovery-Hebel.
---
*Plan erstellt: 2026-05-07. Owner: @till. Letzter Stand-Update: 2026-05-07 nach η.1.*

View file

@ -0,0 +1,110 @@
# cards-server
Cardecky Marketplace + Community backend. Owns the published-deck side
of the Cardecky product (the standalone app at `cardecky.mana.how` is
the client). Phase α is the data skeleton — schema + bootstrap + JWT
auth in place; routes land progressively in Phase β onwards.
For the full design rationale, phasing, and contract decisions see
**[`apps/cards/docs/MARKETPLACE_PLAN.md`](../../apps/cards/docs/MARKETPLACE_PLAN.md)**.
## Tech Stack
| Layer | Tech |
|-------|------|
| Runtime | Bun |
| Framework | Hono |
| Database | PostgreSQL (`mana_platform.cards.*` schema) + Drizzle ORM |
| Auth | JWT via JWKS from mana-auth (EdDSA, jose) |
| Money | mana-credits — never Stripe directly |
## Port: 3072
## Quick Start
```bash
# Schema push (writes to local mana_platform DB)
bun run db:push
# Dev server with watch
bun run dev
# Type check
bun run type-check
```
## Database
Schema: **`cards`** inside the shared `mana_platform` DB. 17 tables across
six logical groups (matching the source files in `src/db/schema/`):
| File | Tables |
|------|--------|
| `authors.ts` | `cards.authors`, `cards.author_follows` |
| `decks.ts` | `cards.decks`, `cards.deck_versions`, `cards.deck_cards` |
| `tags.ts` | `cards.tag_definitions`, `cards.deck_tags` |
| `engagement.ts` | `cards.deck_stars`, `cards.deck_subscriptions`, `cards.deck_forks` |
| `discussions.ts` | `cards.deck_pull_requests`, `cards.card_discussions` |
| `moderation.ts` | `cards.deck_reports`, `cards.ai_moderation_log` |
| `credits.ts` | `cards.deck_purchases`, `cards.author_payouts` |
`co_learn_sessions` (Phase λ) is intentionally not yet in the schema.
Every table is created via `pgSchema('cards')` per the Mana convention.
## Auth model
Three middleware:
- `jwtAuth(authUrl)` — validates Bearer tokens via JWKS. Sets
`c.set('user', { userId, email, role })`. Used on every user-facing
`/v1/*` route.
- `serviceAuth(serviceKey)``X-Service-Key` check for service-to-
service calls (e.g. mana-credits-webhook → cards-server).
- (planned) `optionalAuth` — for routes that should respond
differently when the caller is signed-in but never reject anonymous.
## Phasing (per MARKETPLACE_PLAN §11)
| Phase | What lands | Where |
|-------|-----------|-------|
| **α** | Skeleton + schema + JWT + health | now |
| β | Author publish flow + AI-mod-first-pass | next |
| γ | Discovery (browse, search, tags, follow) | |
| δ | Subscribe + smart-merge | |
| ε | Pull-requests + discussions | |
| ζ | mana-credits marketplace | |
| η | Moderation + trust | |
| θ | Deep AI (auto-tags, embeddings, audio) | |
| ι | Optimisation + scale | |
## Environment Variables
```env
PORT=3072
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/mana_platform
MANA_AUTH_URL=http://localhost:3001
MANA_CREDITS_URL=http://localhost:3061
MANA_LLM_URL=http://localhost:3025
MANA_MEDIA_URL=http://localhost:3015
MANA_NOTIFY_URL=http://localhost:3040
MANA_SERVICE_KEY=dev-service-key
CORS_ORIGINS=http://localhost:5173,http://localhost:5180
# Author payout splits (basis points). Defaults: 80/20 standard,
# 90/10 verified-mana.
AUTHOR_PAYOUT_STANDARD_BPS=8000
AUTHOR_PAYOUT_VERIFIED_BPS=9000
# Community-verified auto-thresholds.
COMMUNITY_VERIFY_STARS=500
COMMUNITY_VERIFY_FEATURED=3
COMMUNITY_VERIFY_SUBSCRIBERS=200
```
## Critical Rules
- **Never call Stripe directly.** All money flows through mana-credits.
- **`/v1` is the public contract** — additive-only changes within v1, breaking changes go to `/v2`.
- **Content-hash everything.** Per-card and per-version SHA-256s drive smart-merge, cache invalidation, and trust.
- **Subscribed Decks are unidirectional.** Author → Subscriber. Forks for the bidirectional case.
- **Verification is binary, not numeric.** Two flags (`verified_mana`, `verified_community`), the UI shows badges. Never invent a "trust score".

View file

@ -0,0 +1,312 @@
# Marketplace-Restore — Playbook
> **Status:** Plan, R0+R1 in Arbeit. Stand 2026-05-09.
> **Vorgänger:** das alte `services/cards-server/` aus `managarten/` (mana-
> monorepo) wurde am 2026-05-08 zusammen mit `apps/cards/` dekommissioniert,
> weil beides eine Kopplung war — siehe Decommission-Commits
> `bc158cb0b` (cards-server), `9cd871749` (apps/cards), `dd1bab09d`
> (cards-core). Rollback-Tag: `cards-decommission-base` im managarten-Repo.
>
> Dieser Plan dokumentiert den **Restore** des Marketplace-Backends in
> die Standalone-Cards-App, additiv zur bestehenden Greenfield-API.
## TL;DR
- **Strategie-B-Klarstellung:** „Kein Code aus mana-monorepo" galt der
Study-/FSRS-/Sync-Schicht (Dexie raus, server-authoritative). Marketplace
war nie davon betroffen — er wurde nur mit-rausgerissen, weil er an
`apps/cards` gekoppelt war.
- **Restore vs. Neubau:** Restore. ~13.000 Zeilen reifer Code, 8 Phasen
in Produktion gelaufen, Plan-Doku in Goldstandard-Qualität (654 Zeilen),
bekannte Limitierungen sauber dokumentiert. Neubau wäre 46 Wochen,
Restore ~14 Tage.
- **Schema-Naming-Entscheidung:** **Eigenes `marketplace`-pgSchema**
in derselben `cards`-DB. Begründung: saubere Read/Write-Trennung,
Backup-Granularität (`pg_dump --schema=marketplace`), RLS-Policies
pro Schema möglich, keine Kollisionen mit den existierenden
`cards.{decks,cards,reviews,…}`-Tabellen.
- **Service-Topologie:** Single-Service. Marketplace-Routen kommen unter
`/api/v1/marketplace/*` in den bestehenden `cards/apps/api`. Kein
zweiter Hono-Prozess, kein zweiter Container. YAGNI bei deinem Volumen.
- **Frontend:** alte Routes 1:1 nach `cards/apps/web/src/routes/`
`/explore`, `/d/[slug]`, `/u/[slug]`, `/me/{published,subscribed,forks,purchases}`,
`/admin/reports`. Imports auf Verdaccio-Pakete umstellen, Theming-
Bridge-Aliase greifen automatisch.
- **Restore-Reihenfolge:** R0 (Doku) → R1 (Schema) → R2 (Auth + Routes
α/β) → R3 (γ + δ) → R4 (ε) → R5 (Frontend) → R6 (Smoke + erste
Cardecky-Decks publishen).
- **Was nicht im ersten Wurf:** Paid Decks (ζ.1) und Moderation-UI
(η.1). Schema-Tabellen kommen mit, aber Code-Pfade bleiben dormant
bis nach erstem Live-Test.
---
## Was die alte Implementation konnte (Phase α–η.1)
Inhalt der archivierten Dokumente — **Original-Wahrheit** unter
`cards/docs/marketplace/archive/`:
| Datei | Was drin |
|---|---|
| `MARKETPLACE_PLAN_2026-05-07.md` | 654-Zeilen-Vollvision: Datenmodell, mana-credits-Flow, Cold-Start-Strategie, Anti-Patterns, Phasen-Status |
| `COMPETITORS_2026-05.md` | 353-Zeilen-Konkurrenz-Analyse: Quizlet, AnkiHub, Brainscape, AnkiPro, AnkiApp, RemNote, Mnemosyne |
| `GUIDELINES.md` | 367 Zeilen Community-Guidelines + Lizenz-Modell (SPDX + Cardecky-Personal-Use-1.0 + Cardecky-Pro-Only-1.0) |
| `cards-server_CLAUDE.md` | 110 Zeilen Tech-Stack-Doku des Original-Service |
Phasen-Status zum Zeitpunkt der Decommission (2026-05-08):
| Phase | Status | Was lief produktiv |
|---|---|---|
| α — Skelett | ✅ live | 17-Tabellen-Schema, JWT-Auth, Container, Tunnel `cardecky-api.mana.how` |
| β — Author-Workflow | ✅ live | Profil-Claim, Publish, Lizenz-Picker (SPDX), Preis-Eingabe, AI-Mod-First-Pass |
| γ — Discovery | ✅ live | `/explore`, Stars, Follows, Author-Profile, Trending, Search (ILIKE) |
| δ — Subscribe + Smart-Merge | ✅ live | Pull, Diff-View „+N · ~N · N", FSRS-State erhalten über Karten-Hash-Diff |
| ε — PRs + Discussions | ✅ live | „✏️ Verbessern" auf jeder Karte, Author-Merge, Inline-Threads, Notify-Mails |
| ζ.1 — Paid Decks | ✅ live | 4-Schritt-Reserve→Purchase→Commit→Grant, 80/20-Split (90/10 für `verified_mana`) |
| η.1 — Moderation | ✅ live | Reports, Admin-Inbox, Takedown-Workflow (kaskadiert auf PRs), Author-Ban |
Bekannte Limitierungen (siehe `MARKETPLACE_PLAN_2026-05-07.md` §13a):
PR-Merge-stale-blind, Reconciler-Lücke bei Paid-Pipeline, Mention-System
fehlt, Discussion-Threading 1-Level, kein Refund-Self-Service. Alles
dokumentiert, nichts unbekannt.
---
## Architektur-Anpassungen für den Restore
### 1. DB-Topologie: eigenes `marketplace`-pgSchema
**Alt** (in `mana_platform`-DB, geteilt mit allen mana-Services):
```
mana_platform.cards.{authors, decks, deck_versions, …} — 17 Tabellen
```
**Neu** (in standalone `cards`-DB des Standalone-Repos):
```
cards.cards.{decks, cards, reviews, media_files, tags, imports} — Greenfield, bleibt
cards.marketplace.{authors, decks, deck_versions, deck_cards, …} — Restore, neu
```
Begründung für ein **eigenes pgSchema** statt Tabellen-Prefix
(`published_decks`, `published_deck_versions`, …):
- **Sauberer Read-Path.** Public-Endpoints sehen nur das `marketplace`-
Schema. Greenfield-Code (private Decks/Karten/Reviews) sieht
ausschließlich `cards`. Kein Risiko, dass ein `SELECT * FROM decks`
versehentlich beide Welten mischt.
- **Backup-Granularität.** `pg_dump --schema=marketplace` exportiert
nur den Marketplace-Stand für Compliance/Recovery. Privater Lern-
Stand der User bleibt unangetastet.
- **RLS-Policies pro Schema** — falls wir je Row-Level-Security
einführen für public-decks-take-down-Workflows, ist das pro Schema
konfigurierbar.
- **Drizzle-kit-Push-Disziplin.** `schemaFilter: ['cards', 'marketplace']`
hält beide Pushes sauber. Schema-Drift fängt sich auf Schema-Ebene.
Drizzle-Variablennamen halten den `public`-Prefix aus dem alten Code:
`publicDecks`, `publicDeckVersions`, `publicDeckCards`. So bleibt das
intent klar, und Imports kollidieren nicht mit `cards.decks` aus dem
Greenfield.
### 2. Auth-Modell: identisch, kein Refactor
Alter cards-server: JWKS-Cache gegen `mana-auth`. Greenfield-cards-api:
JWKS-Cache gegen `mana-auth`. Identisch. Service-Key-Auth (`X-Service-Key`)
für Mana-Webhooks ebenfalls 1:1 übernommen.
Optional-Auth-Middleware für Public-Endpoints (`/explore`, `/d/:slug`)
muss aus dem alten Code mitkommen — der erlaubt anonymen Read und
gibt zugleich personalisierte Daten (z.B. „Bist du Subscriber?")
wenn ein Bearer mit-übermittelt ist.
### 3. Service-Topologie: Single-Service
Alle Marketplace-Routen unter `/api/v1/marketplace/*` in
`cards/apps/api/src/routes/marketplace/`:
```
cards/apps/api/src/routes/marketplace/
├── authors.ts
├── decks.ts (publish, list, version reads)
├── engagement.ts (stars, subscribe, fork)
├── discussions.ts (card-discussions threads)
├── pull-requests.ts
├── moderation.ts (reports + admin)
├── purchases.ts (paid decks — dormant in R3, aktiv ab späterer Welle)
└── explore.ts (discovery + search)
```
Hauptserver-Mount in `cards/apps/api/src/index.ts`:
```ts
app.route('/api/v1/marketplace/authors', authorsRouter())
app.route('/api/v1/marketplace/decks', decksRouter())
// …
```
Vorteile: ein Prozess, ein Container, ein Tunnel-Endpoint
(`cardecky-api.mana.how`), eine JWT-Validierung, eine Drizzle-DB-Connection.
### 4. Frontend: additive Routes, gleicher Stack
`cards/apps/web/` ist SvelteKit + Svelte 5 (runes-only). Alter
`apps/cards/apps/web/` war es auch. Routen werden 1:1 übernommen,
mit drei Anpassungen:
- **Imports:** `@mana/shared-*` kommt heute aus Verdaccio (npm.mana.how).
`pnpm add @mana/shared-ui@^0.1.1 @mana/shared-share-protocol …`.
- **Theming:** alte Components nutzten alte Token. Greenfield-Bridge-
Aliase in `app.css` mappen die meisten alten Token aufs 12er-Mana-
Vokabular. Test im Browser, ggf. Anpassungen.
- **Auth-Hook:** `dev-stub.svelte.ts` bleibt für Phase-2-Lücke. Sobald
echte mana-auth-Login-Flow ausgerollt ist (siehe `STATUS.md`),
weicht der Stub für `@mana/shared-auth`.
### 5. Hash-Implementierung: bestehende `@cards/domain` benutzen
Alter `cards-server/src/lib/hash.ts`: eigenständige SHA-256-Implementierung.
Greenfield: `cardContentHash` in `@cards/domain` (Web-Crypto, deterministisch).
Beim Restore: `lib/hash.ts` **nicht** mit-übernehmen, sondern
Marketplace-Code auf `cardContentHash` aus `@cards/domain` umbiegen.
Eine Hash-Definition für die ganze App.
### 6. mana-Service-Calls: identische Pattern
Alter cards-server rief mana-credits, mana-llm, mana-media, mana-notify
über `MANA_*_URL`-env-Vars. Greenfield-cards-api macht das genauso.
Code 1:1 übernehmen.
---
## Wellen-Plan
| Welle | Zustand | Was passiert | Blocker |
|---|---|---|---|
| **R0** | 🟡 in Arbeit | Doku-Restore: Archive aus cards-decommission-base + dieser Plan + Strategie-B-Klarstellung | — |
| **R1** | ⏸ pending | Schema-Restore: 7 Schema-Files in `cards/apps/api/src/db/schema/marketplace/`, drizzle-kit push grün gegen lokale `cards`-DB | R0 |
| **R2** | ⏸ pending | Backend Phase α + β: Author-Profile + Publish + AI-Mod-Stub | R1 |
| **R3** | ⏸ pending | Backend Phase γ + δ: Discovery + Subscribe + Smart-Merge | R2 |
| **R4** | ⏸ pending | Backend Phase ε: Pull-Requests + Card-Discussions | R3 |
| **R5** | ⏸ pending | Frontend-Routes: `/explore`, `/d/[slug]`, `/u/[slug]`, `/me/{published,subscribed,forks}` | R4 |
| **R6** | ⏸ pending | E2E-Smoke: erstes Cardecky-Deck publishen, von Till's Account subscriben, Smart-Merge testen | R5 |
**Aufwand-Schätzung gesamt: ~14 Tage Real-Arbeit.**
Bewusst aus dem ersten Restore-Wurf rausgelassen:
- **ζ.1 Paid Decks** — Schema-Tabellen kommen in R1 mit, aber Routes/UI
bleiben dormant. Re-Aktivierung als eigene Welle nach Live-Validation.
Begründung: mana-credits-Integration ist heikel (4-step-Pipeline mit
reservation-commit-grant), Author-Erlöse sind ein Verein-Compliance-
Thema (Steuern, AGB, Refund-Policy), und solange Cardecky synthetic
Decks publisht, gibt's keinen Need.
- **η.1 Moderation-UI** — Schema-Tabellen + API-Endpoints kommen mit,
Admin-Frontend (`/admin/reports`) wird ausgelassen bis erste echte
User da sind. Take-Down via SQL für die ersten Wochen.
- **θ Deep AI** (Auto-Tags, Embeddings, TTS) — bleibt explizit später-
Phase, war im Original-Plan auch nicht für den ersten Wurf vorgesehen.
---
## Cardecky-Skill-Integration
Der `/cards-deck`-Skill (siehe `~/.claude/skills/cards-deck/SKILL.md`)
produziert Decks unter dem Cardecky-Plattform-User. Beim Marketplace-
Restore wird Cardecky **automatisch zum Marketplace-Author**:
1. Bei R2 wird ein Init-Skript einen `marketplace.authors`-Row für
Cardecky-User-ID anlegen (`slug='cardecky'`, `display_name='Cardecky'`,
`pseudonym=false`, `verified_mana=true` — der Verein vergibt das
Badge an seinen eigenen KI-Author).
2. Skill-Stufe 5 (Publish) wird erweitert um einen optionalen Schritt:
nach Anlegen des privaten Decks kann der Skill ein `POST
/api/v1/marketplace/decks/:id/publish` mit `semver=1.0.0` und einem
auto-generierten Changelog hinterher schicken — dann ist das Deck
sofort im `/explore` sichtbar.
3. Default bleibt aber „nur privat anlegen", weil:
- das Validate-Stage (Stufe 4) eine menschliche Sichtung verdient,
bevor 30 Karten öffentlich werden;
- Reviewer-Stops nach Stufe 3 sind zwingend.
Der Skill braucht keine Architektur-Änderung — er addiert nur einen
optionalen 6. Schritt.
---
## Lizenz-Modell (aus dem Original übernommen)
Aus `MARKETPLACE_PLAN_2026-05-07.md` §3 + `GUIDELINES.md`:
| SPDX-ID | Erlaubt | Wann |
|---|---|---|
| `CC0-1.0` | alles, kein Attribution-Pflicht | für Public-Domain-fähige Karten |
| `CC-BY-4.0` | alles, mit Attribution | meist gewählt für Wissens-Decks |
| `CC-BY-SA-4.0` | alles, ShareAlike + Attribution | für Wikipedia-derivierte Decks |
| `Cardecky-Personal-Use-1.0` | nur persönlicher Lern-Use, kein Re-Publish | **Default für kostenlose Decks** |
| `Cardecky-Pro-Only-1.0` | nur via Kauf, kein Re-Publish, kein Fork | **Pflicht für paid Decks** (DB-CHECK enforced) |
Der DB-CHECK auf `decks.price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0'`
ist im Schema beibehalten — Code-Bug kann nicht stillschweigend ein
Paid-Deck mit CC-Lizenz publishen.
---
## Cold-Start-Strategie (aus dem Original)
Kommt im ersten Restore-Wurf nicht aktiv zum Tragen, aber der Original-
Plan §9 hat drei Hebel definiert, die bei Re-Launch greifen:
1. **Verein-Seed-Decks** — 50 hochwertige Cardecky-published Decks
(Sprachen, Geschichte, Allgemeinwissen, Programmierung). Der
`/cards-deck`-Skill ist genau das Werkzeug dafür.
2. **Anki-Top-100-Import-Service** — populäre CC-BY-Anki-Decks mit
Original-Author-Attribution importieren, Original-Author bekommt
`verified_mana`-Badge bei Registrierung.
3. **Influencer-Outreach** — 1020 Anki-Power-Authoren (AnKing &
Konsorten) gezielt ansprechen, sehr Author-freundlicher Cut.
Hebel 2 + 3 sind Wachstumsmaßnahmen, nicht Phase-1. Hebel 1 ist
sofort umsetzbar mit dem bestehenden Skill.
---
## Anti-Patterns aus dem Original-Plan §13 (gelten weiter)
- **Kein 1-5-Sterne-Rating-System.** Stars (Bookmark) ja, Bewertungen nein.
- **Kein Reddit-Style-Voting** auf Karten/PRs/Discussions. Hacker-News-Effekt.
- **Kein „Karten der Woche" allein-algorithmisch.** Editorial + Trending-
Liste, aber niemals reiner Algo-Feed.
- **Kein Anki-Bashing im Marketing.** Bridge nicht Burning.
- **Keine Pflicht-Klarnamen.** Pseudonyme bleiben gleichberechtigt.
- **Kein Marketplace-Cut über 30 %.** Standard 80/20, verified 90/10.
---
## Offene Punkte
- **PR-Merge-stale-blind aus dem Original.** Bekannte Limitierung: wenn
Author zwischen PR-Open und Merge selbst eine Karte ändert, deren
`previousContentHash` der PR matched, gewinnt stumm der PR. Im
ersten Restore-Wurf so übernehmen wie war; späterer Fix via
optimistic locking auf `baseVersionId` der PR-Row mit Reject bei
Mismatch.
- **Reconciler-Cron für Paid-Pipeline-Inkonsistenzen.** Original §13a
beschreibt das Loch: bei Commit-/Grant-Failure nach Schritt 2 bleibt
eine Purchase-Row mit `creditsTransaction = null`. Beim Restore
initial nicht aktiv (Paid-Decks dormant), aber sobald Phase ζ
reaktiviert wird, ist Reconciler ein muss-haben.
- **`cards-decommission-base`-Tag im managarten-Repo.** Falls jemand
managarten löscht oder das Tag nochmal entfernt, geht der Restore-
Pfad verloren. Empfehlung: Schema-Files + ausgewählte Code-Snippets
einmal nach `cards/docs/marketplace/archive/` kopieren (Doks sind
schon drin, Code-Files folgen optional bei R1+R2).
- **Schema-Migrations-Pfad bei späterer Drizzle-Version.** Greenfield-
Cards plant Migration auf Drizzle 0.45/zod-4 mit der Plattform mit
(`mana/docs/MIGRATION_DRIZZLE_ZOD.md`). Marketplace-Schema kommt mit
Drizzle 0.38 — sollte mit upgradeen, idealerweise atomar.
- **Karten-Hash-Konsistenz zwischen Greenfield und Marketplace.**
Greenfield-`@cards/domain` `cardContentHash` ist die SoT. Marketplace-
`deck_cards.content_hash` muss mit demselben Algorithmus berechnet
werden — sonst funktioniert Smart-Merge nicht. Beim R2-Port-Pass
testen, dass `cardContentHash({type,fields})` aus `@cards/domain`
byte-identisch ist mit dem alten `cards-server/src/lib/hash.ts`. Wenn
nicht: alten Code anpassen, nicht `@cards/domain` brechen.