- Go 60%
- Svelte 22.5%
- TypeScript 9.2%
- CSS 2.8%
- Dockerfile 1.9%
- Other 3.6%
| backend | ||
| scripts | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .env.production.example | ||
| .gitignore | ||
| Caddyfile | ||
| docker-compose.example.yml | ||
| docker-compose.production.example.yml | ||
| Makefile | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
MeshCore Ninja Account MVP
A self-hosted identity foundation for the MeshCore Ninja ecosystem, built with a Go backend, Svelte 5/SvelteKit frontends, Tailwind CSS, and an embedded SQLite database (PostgreSQL optional).
The MVP has two separate applications:
account.meshcore.ninja— registration, login, profile, passkeys, linked identities, and session management.auth-test.meshcore.ninja— an independent first-party OIDC client proving that another MeshCore Ninja service can use the account system while keeping its own session.
Included
- Email magic-link registration and login
- Discoverable passkeys / usernameless WebAuthn login
- GitHub OAuth login and explicit account linking
- Ethereum EOA login and linking with an EIP-4361-style message and EIP-191 signature verification
- Permanent internal account UUID independent of every login provider
- Profile editing: username, display name, avatar URL, region/community, and bio
- Linked identity removal with protection against deleting the last authentication method
- Passkey management and active-session revocation
- Minimal first-party OpenID Connect Authorization Code flow with mandatory PKCE S256
- Ed25519-signed ID and access tokens with discovery and JWKS endpoints
- Database-agnostic storage (SQLite by default, PostgreSQL optional), Mailpit, Caddy routes, development Compose, and production containers
Architecture
email / passkey / GitHub / Ethereum
│
▼
account.meshcore.ninja
Go API + account portal
│
OIDC code + PKCE
│
▼
auth-test.meshcore.ninja
independent app session
Repository layout:
backend/ Go account API and OIDC issuer
web/account/ Svelte 5 account portal
web/auth-test/ Independent SvelteKit OIDC test client
docker-compose.yml Mailpit (and optional PostgreSQL) for development
docker-compose.production.yml Full container deployment example
Caddyfile Production subdomain routing
Local development
Requirements:
- Go 1.25+
- Node.js 22+
- pnpm via Corepack
- Docker (for Mailpit; the database is embedded SQLite by default)
cp .env.example .env
docker compose up -d mailpit
corepack enable
pnpm install
The backend creates and migrates the SQLite database on first run. To use
PostgreSQL instead, set DATABASE_URL=postgres://… in .env and start the
optional container with docker compose --profile postgres up -d postgres.
Run the backend:
set -a
source .env
set +a
make backend
Run both frontends in another terminal:
make web
Open:
- Account portal:
http://localhost:5173 - Auth test client:
http://localhost:5174 - Mailpit:
http://localhost:8025
The account Vite server proxies /api, /oauth, /.well-known, and /healthz to the Go service on port 8080.
First login
Without GitHub configuration, start with email:
- Enter an email at
http://localhost:5173/login. - Open Mailpit at
http://localhost:8025. - Follow the one-time login link.
- Add a passkey from Security.
- Open
http://localhost:5174and test the independent OIDC login.
DEV_EXPOSE_MAGIC_LINK=true also shows the development login URL directly in the account UI. Disable this outside local development.
GitHub OAuth
Create a GitHub OAuth App:
Homepage URL: http://localhost:5173
Authorization callback: http://localhost:5173/api/auth/github/callback
Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in .env.
Production callback:
https://account.meshcore.ninja/api/auth/github/callback
GitHub login proves identity only. Future automatic PR creation should use a separate MeshCore Ninja GitHub App and explicit permissions rather than reusing the login credential.
Passkeys
WebAuthn works on HTTPS origins and on localhost.
Development:
WEBAUTHN_RP_ID=localhost
WEBAUTHN_ORIGINS=http://localhost:5173
Production:
WEBAUTHN_RP_ID=meshcore.ninja
WEBAUTHN_ORIGINS=https://account.meshcore.ninja
The MVP requests discoverable credentials and required user verification. Passkey ceremonies are completed only by the account origin.
Ethereum login
The browser requests a wallet signature over a server-generated, short-lived sign-in message containing the account domain, URI, chain ID, nonce, issue time, and expiration. The Go backend verifies the recovered EOA address and consumes the challenge once.
The MVP supports normal externally owned accounts. Contract-wallet verification through ERC-1271 is intentionally outside the initial scope.
OIDC test client
The account service exposes:
/.well-known/openid-configuration
/.well-known/jwks.json
/oauth/authorize
/oauth/token
/oauth/userinfo
The initial client is intentionally fixed:
client_id: meshcore-ninja-auth-test
redirect_uri: http://localhost:5174/callback
flow: Authorization Code + PKCE S256
scopes: openid profile email
auth-test.meshcore.ninja validates the ID token against the issuer JWKS, checks issuer, audience and nonce, and then creates a separate HttpOnly local session cookie. ACCOUNT_BACKCHANNEL_URL may point token and JWKS requests at the internal Go service while the public issuer remains https://account.meshcore.ninja.
Signing key
Development generates an ephemeral OIDC key when none is supplied. Generate a persistent production key with:
make keygen
Put the printed base64url value into OIDC_ED25519_PRIVATE_KEY. Store it as a secret and back it up securely.
Tests and checks
make test
This runs Go tests and Svelte checks. A PKCE RFC test vector and internal redirect-safety tests are included.
Production example
cp .env.production.example .env
# Fill all secrets and SMTP/GitHub settings.
docker compose -f docker-compose.production.yml up -d --build
Point DNS for both subdomains to the Caddy host:
account.meshcore.ninja
auth-test.meshcore.ninja
Security boundary
This MVP deliberately avoids a shared parent-domain login cookie. The account service owns its own session; every client receives a short-lived authorization code and creates its own secure session. All application data should reference the internal account UUID, never an email, GitHub ID, passkey credential ID, or Ethereum address.
Before public production use, commission an independent security review and add distributed rate limiting, structured audit events, transactional email delivery with TLS/authentication, key rotation, backup and restore drills, abuse controls, and browser-level end-to-end tests. The OIDC surface is deliberately narrow and intended only for registered first-party clients; it is not a general-purpose public identity provider.