Architecture
Crate structure
crates/
link-shortener/ Entry point, wires everything together
link-shortener-api/ Axum routes, OIDC auth, OpenAPI via utoipa
link-shortener-store/ Sea-ORM repository layer
entity/ Generated Sea-ORM entity models (do not edit)
migration/ Sea-ORM database migrations
Auth flow
- The user clicks “Log in” on the frontend.
- The browser navigates to a protected route such as
/api/me, which is behindOidcLoginLayer. - axum-oidc redirects the browser to Keycloak. The redirect URI registered with the client is the shared OAuth relay, and the OAuth state carries a CSRF token plus the app callback to return to (
{APP_URL}/auth/callback). - The user authenticates with Keycloak.
- Keycloak redirects to the OAuth relay with an authorization code.
- The relay reads the return target from the state and forwards the code to
{APP_URL}/auth/callback. /auth/callbackexchanges the code for tokens and creates a server side session.- The browser is redirected back to the app, now authenticated.
- The frontend calls
/api/meagain and receives user info from the session.
API design
Routes are registered with utoipa-axum for automatic OpenAPI schema generation. The schema is served at /openapi.json and is browsable at /swagger-ui.
Protected routes require a valid session (enforced by OidcLoginLayer):
GET /api/meGET /api/linksPOST /api/linksPATCH /api/links/{id}DELETE /api/links/{id}
Public routes do not require authentication:
GET /api/healthGET /auth/callback(OIDC redirect handler)GET /{slug}(slug redirect, handled by the router fallback)
Static files and slug redirects
In production the backend serves the built SPA itself. When STATIC_DIR is set, a ServeDir serves the static files and uses the slug handler as its not found service. The slug handler checks whether the path is a single segment matching a slug in the database; if so it returns a 307 redirect, otherwise a 404. When STATIC_DIR is unset, as in local development, the SPA is served by the Vite dev server and the backend fallback is the slug handler alone.
Frontend
The frontend is built with Svelte 5 and Vite on the Deno toolchain. Auth state is determined by calling /api/me on mount. The Vite dev server proxies /api, /auth, /swagger-ui, and /openapi.json to the backend on port 3000.
Once the backend is running, deno task generate-api generates TypeScript types from the OpenAPI schema via openapi-typescript. The openapi-fetch library provides a typed client for making API calls.