Organizations can pin the source they mirror from when more than one source is connected.
- New nullable organizations.source_id column (migration 0020_org_source), backfilled only when an organization's repositories agree on exactly one source.
- NULL means every source; a pin whose source was deleted falls back to the same. A valid pin scopes the org mirror job, the repository count breakdowns and org deletion to that source.
- POST /api/sync/organization persists the pin, PATCH /api/organizations/:id sets or clears it, the bulk import stamps discovered orgs and clears the pin when a second source lists the same org.
- UI, only with more than one source connected: a Source picker in the Add Organization dialog, a per-card inline source editor with an Every source option, and per-org View on source links.
- The org mirror repository query now filters by user.
- Shared provider icon map in src/lib/source-providers/icons.ts.
Changing a repository's or an organization's destination used to change a
label and nothing else: the mirror stayed where it was, sync failed once
someone transferred it in Gitea, and Retry created a second copy.
Sync now looks for a mirror of the same source anywhere on the destination
before failing, and the mirror path does the same before creating a copy.
Reconcile reports moved mirrors and can record their new location.
The destination editors offer to transfer the existing mirror on Gitea or
Forgejo, with a confirmation for organizations. Pending transfers, when the
token cannot create repositories under the new owner, are reported, and
sync follows the mirror once accepted. Nothing is deleted on either side.
Four new documentation pages (sources, destinations, API and API keys, cleanup and reconcile), updates to the configuration, environment variables, quickstart, architecture and advanced pages, refreshed configuration screenshots and new ones for the destination dropdown and the API keys card. Also fixes the path prefix variable name in docs/API.md and the stale follow-up note in docs/SOURCE_PROVIDERS.md.
The GitLab and Gitea / Forgejo sources and the Forgejo, GitHub and GitLab destinations show a beta pill and beta wording; the docs say GitHub to Gitea is the supported path. Testing every source and destination pair against real hosts turned up three bugs, fixed here: the pull mirror path never recorded a row's destination host, reconcile counted rows mirrored to a previous destination as missing, and the first push into a freshly created GitLab project failed because the host reports it as not found for a couple of seconds.
Compare what Gitea or Forgejo holds under every owner this account
mirrors into with the repositories table. Mirrors of the configured
source that have no row are reported as untracked and can be adopted;
rows marked mirrored whose repository is gone are confirmed with a
direct check and can be reset to imported so the next run recreates
them. Native repositories and mirrors of other hosts are listed and
never touched, and nothing is deleted or archived. Exposed as
POST /api/cleanup/reconcile and as a dialog on the Automation tab.
Closes#284
Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
* feat(auth): API keys for programmatic access
Adds the @better-auth/api-key plugin so scripts and CI pipelines can call
the existing endpoints with an x-api-key header instead of a session
cookie. Keys are owned by a user, hashed at rest, prefixed gm_, never
expire unless an expiry is chosen, and are not rate limited. A small
guard plugin refuses key management calls that arrive with a key, so a
leaked key cannot mint or revoke keys.
New API Keys section on the Authentication tab with create, show once,
copy and revoke. Migration 0017 adds the api_keys table with the
validator fixture. docs/API.md documents the header and the calls
automation needs. An e2e spec covers create, use, refuse and revoke over
HTTP. bun.nix regenerated for the new package.
Closes#314
Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
* fix(e2e): send Origin on cookie-authenticated key management calls
Better Auth rejects a cookie-authenticated POST without an Origin header (403 MISSING_OR_NULL_ORIGIN). Browsers always send one, the Playwright request context does not, so the spec sets it on the create and delete calls. Also asserts the guard's 403 code and documents the Origin requirement for scripts that manage keys with a session.
Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX