尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Tracks where you read each manga and webtoon — the app, the site, or a physical copy. MyAnimeList already knows what you read and how far along you are; it doesn't know that chapter 42 was on WEBTOON and the rest was on Tapas.
Your account lives in Supabase. MyAnimeList is a connection you link to it, not a login — so you can unlink and relink without losing anything, and account recovery works normally.
AniList can be connected the same way. MyAnimeList stays the library's source of truth; AniList is kept in step with it — progress saved in the app is mirrored there, and Settings → Copy between sites reconciles the two lists (both ways, newest edit wins, or one way). Nothing is ever deleted from either site.
Deliberate boundaries, so a feature that crosses one gets questioned rather than quietly built:
/discover all call verifySession(). Only the
landing page, the auth screens and the legal pages are public. A new page
belongs in the gated set unless there is a reason it cannot be.user_entries and entry_sources, so
that is enforced by the database rather than by which screens exist./discover is the one shared surface, and it is editorial. Curated
collections and tags are admin-authored (owner_id is null, see
supabase/migrations/20260909000000_admins_and_tags.sql), shown to everyone
signed in, and writable only by an admin granted out of band via
yarn grant:admin. It is a shelf the maintainer curates for readers, not a
place readers publish to each other.Shared catalog data is not user-to-user contact: media_titles is one row
per MAL title readable by any signed-in user, because it describes a comic
rather than a person. The line is whether a row says anything about who reads
what.
yarn install
cp .env.example .env.local # then fill it in
yarn dev
.env.example documents every variable plus the Supabase dashboard settings a
deploy needs.
To populate a demo account with a sample library (12 titles and their source
assignments), set TEST_USER_EMAIL / TEST_USER_PASSWORD and run:
yarn seed:demo # upsert; safe to re-run
yarn seed:demo --reset # clear that user's entries first
It is scoped to that one user and writes a token-less mal_connections row so
/library renders the grid rather than the connect CTA. See the header comment
in scripts/seed-demo.ts for why that row cannot trigger a destructive sync.
Database migrations live in supabase/migrations/ and are applied with the
Supabase CLI:
supabase migration list --linked # compare local and remote
supabase db push # apply pending migrations
Browser ─▶ proxy.ts cheap cookie check only (no DB, no network)
│
▼
app/(app)/* ─▶ lib/auth/dal.ts real auth, cached per request
│
├─▶ Supabase (RLS) library, entries, sources
└─▶ Server Actions ─┬▶ MalClient ─▶ api.myanimelist.net
└▶ AniListClient ─▶ graphql.anilist.co
Two-layer auth. proxy.ts only checks whether a session cookie exists, to
avoid flashing a protected page. It proves nothing. Real enforcement is
verifySession() in lib/auth/dal.ts, called from the authed layout, every
protected page, and the top of every server action — actions are
independently reachable HTTP endpoints, so a layout check does not protect them.
| Table | Holds |
|---|---|
profiles | one row per auth user |
mal_connections | links an account to a MAL account (active / disconnected / needs_reauth) |
private.mal_tokens | OAuth tokens; unreachable via the Data API |
anilist_connections | links an account to an AniList account, same statuses as MAL |
private.anilist_tokens | AniList access token; same isolation as mal_tokens |
media_titles | shared catalog — one row per MAL title, owned by nobody |
user_entries | one user's progress against a title |
entry_sources | where that user reads it — the product |
sources | global catalog + per-user custom sources under "Other" |
The catalog is deliberately separate from progress: ten users tracking the same title share one metadata row instead of ten copies. Measured on representative data, that is ~58% smaller than the denormalised alternative.
Each of these was found the hard way; all are load-bearing.
expires_in is the refresh window (~28 days), not the access
token's — despite the docs saying "one hour". Never compute access-token
expiry from it. MalClient refreshes reactively on 401.plain only — the code challenge equals the verifier. This
looks like a bug and is not; "fixing" it to S256 breaks login.PUT /manga/{id}/my_list_status is form-encoded, not JSON.private schema is unreachable via PostgREST (406 even with the secret
key), which is why token access goes through SECURITY DEFINER RPCs granted
only to service_role.middleware.ts to proxy.ts. Most Supabase tutorials
still put getUser() in middleware; this app deliberately does not.@theme inline resolves at parse time, so font families must be literal
strings — var(--font-*) silently resolves to nothing.refresh_token, but AniList does not honour it. On expiry the
connection goes needs_reauth and the user reconnects.scoreRaw (0–100), never score, which is
read in the user's own score format — a 7 sent to a POINT_100 user lands as
7/100.idMal).
An AniList title with no MAL counterpart cannot be synced — common for
Korean webtoons — and is reported as skipped rather than guessed at.