Server-rendered authentication UI using Leaf templates with customizable styles and themes.
The Views feature provides pre-built HTML templates for authentication flows, rendered server-side using Vapor's Leaf templating engine. Views integrate with other Passage features (Account, Restoration, Passwordless, Linking) to provide a complete web-based authentication experience without building custom frontends.
Key capabilities:
- Pre-built login, registration, password reset, and magic link forms
- Four visual styles: Neobrutalism, Neomorphism, Minimalism, Material
- 20+ color themes with light/dark mode support
- OAuth account linking UI (select account, verify identity)
- Automatic error/success message handling via query params
- Custom redirect configuration per view
Passage.Configuration(
// ... other config ...
views: .init(
login: .init(
style: .material,
theme: .init(colors: .oceanLight),
redirect: .init(onSuccess: "/dashboard"),
identifier: .email
),
register: .init(
style: .material,
theme: .init(colors: .oceanLight),
identifier: .email
),
passwordResetRequest: .init(
style: .material,
theme: .init(colors: .oceanLight)
),
passwordResetConfirm: .init(
style: .material,
theme: .init(colors: .oceanLight)
),
magicLinkRequest: .init(
style: .material,
theme: .init(colors: .oceanLight)
),
magicLinkVerify: .init(
style: .material,
theme: .init(colors: .oceanLight),
redirect: .init(onSuccess: "/dashboard")
),
linkAccountSelect: .init(
style: .material,
theme: .init(colors: .oceanLight)
),
linkAccountVerify: .init(
style: .material,
theme: .init(colors: .oceanLight)
)
)
)| View | Purpose | Related Feature |
|---|---|---|
login |
Email/phone/username + password login | Account |
register |
User registration form | Account |
passwordResetRequest |
Request password reset code | Restoration |
passwordResetConfirm |
Enter code and new password | Restoration |
magicLinkRequest |
Request magic link email | Passwordless |
magicLinkVerify |
Magic link verification result | Passwordless |
linkAccountSelect |
Select account to link (OAuth) | Linking |
linkAccountVerify |
Verify ownership for linking | Linking |
passkeyGuestRegistration |
Create an account with a passkey (WebAuthn registration ceremony, navigator.credentials.create()) — public signup flow only; the authenticated "add passkey" flow is JSON-only and has no Leaf view |
Passkey |
passkeyAuthentication |
Sign in with an existing passkey (WebAuthn authentication ceremony, navigator.credentials.get(), discoverable flow) |
Passkey |
| Option | Type | Default | Description |
|---|---|---|---|
style |
Style |
- | Visual style (required) |
theme |
Theme |
- | Color theme (required) |
redirect |
Redirect |
nil |
Custom redirect URLs |
identifier |
Identifier.Kind |
- | Login/register identifier type |
| Option | Type | Default | Description |
|---|---|---|---|
onSuccess |
String? |
nil |
URL to redirect after success |
onFailure |
String? |
nil |
URL to redirect after failure |
Views register GET routes that render HTML pages:
| Method | Path | View | Description |
|---|---|---|---|
| GET | /auth/login |
login |
Login form |
| GET | /auth/register |
register |
Registration form |
| GET | /auth/reset/email/request |
passwordResetRequest |
Request reset (email) |
| GET | /auth/reset/phone/request |
passwordResetRequest |
Request reset (phone) |
| GET | /auth/reset/email/verify |
passwordResetConfirm |
Confirm reset (email) |
| GET | /auth/reset/phone/verify |
passwordResetConfirm |
Confirm reset (phone) |
| GET | /auth/magic-link/request |
magicLinkRequest |
Request magic link |
| GET | /auth/connect/link/select |
linkAccountSelect |
Select account to link |
| GET | /auth/connect/link/verify |
linkAccountVerify |
Verify account ownership |
| GET | /auth/passkey/guest/registration/begin |
passkeyGuestRegistration |
Passkey guest registration form |
| GET | /auth/passkey/authenticate/begin |
passkeyAuthentication |
Passkey sign-in form (single button, no identifier input — discoverable flow) |
Four built-in visual styles are available:
| Style | Description |
|---|---|
.neobrutalism |
Bold borders, strong shadows, high contrast |
.neomorphism |
Soft shadows, subtle depth, modern feel |
.minimalism |
Clean lines, whitespace-focused, understated |
.material |
Google Material Design inspired |
Each theme has light and dark variants:
| Theme | Light | Dark |
|---|---|---|
| Default | .defaultLight |
.defaultDark |
| Ocean | .oceanLight |
.oceanDark |
| Forest | .forestLight |
.forestDark |
| Sunset | .sunsetLight |
.sunsetDark |
| Midnight | .midnightLight |
.midnightDark |
| Cherry | .cherryLight |
.cherryDark |
| Lavender | .lavenderLight |
.lavenderDark |
| Mocha | .mochaLight |
.mochaDark |
| Slate | .slateLight |
.slateDark |
| Ember | .emberLight |
.emberDark |
| Mint | .mintLight |
.mintDark |
| Plum | .plumLight |
.plumDark |
| Amber | .amberLight |
.amberDark |
| Sage | .sageLight |
.sageDark |
| Rose | .roseLight |
.roseDark |
| Indigo | .indigoLight |
.indigoDark |
| Coral | .coralLight |
.coralDark |
Create custom themes by defining all color properties:
let customTheme = Theme(
colors: .init(
primary: "#6200EE",
onPrimary: "#FFFFFF",
secondary: "#03DAC6",
onSecondary: "#000000",
surface: "#FFFFFF",
onSurface: "#000000",
onSurfaceVariant: "#616161",
background: "#F5F5F5",
onBackground: "#000000",
error: "#B00020",
onError: "#FFFFFF",
warning: "#FF9800",
onWarning: "#000000",
success: "#4CAF50",
onSuccess: "#FFFFFF",
outline: "#BDBDBD"
),
overrides: [
.dark: .init(colors: .defaultDark)
]
)| Property | Usage |
|---|---|
primary |
Primary action buttons, links |
onPrimary |
Text/icons on primary background |
secondary |
Secondary actions, accents |
onSecondary |
Text/icons on secondary background |
surface |
Card backgrounds, form containers |
onSurface |
Primary text on surfaces |
onSurfaceVariant |
Secondary/muted text |
background |
Page background |
onBackground |
Text on page background |
error |
Error states, validation errors |
onError |
Text on error background |
warning |
Warning states |
onWarning |
Text on warning background |
success |
Success states, confirmations |
onSuccess |
Text on success background |
outline |
Borders, dividers |
flowchart LR
subgraph Authentication
A[Login View] -->|POST /login| B[Account Feature]
C[Register View] -->|POST /register| B
end
subgraph "Password Reset"
D[Reset Request View] -->|POST /reset/email| E[Restoration Feature]
F[Reset Confirm View] -->|POST /reset/email/verify| E
end
subgraph Passwordless
G[Magic Link Request] -->|POST /magic-link| H[Passwordless Feature]
I[Magic Link Verify] -->|GET /magic-link/verify| H
end
subgraph "OAuth Linking"
J[Link Select View] -->|POST /link/select| K[Linking Feature]
L[Link Verify View] -->|POST /link/verify| K
end
B --> M[Dashboard]
E --> A
H --> M
K --> M
Each view receives a Context with theme colors and view-specific params:
struct Context<Params>: Encodable {
let theme: Theme.Resolved // Resolved colors for current brightness
let params: Params // View-specific parameters
}Views read success/error messages from query parameters:
| Parameter | Description |
|---|---|
success |
Success message to display |
error |
Error message to display |
identifier |
Pre-filled identifier value |
code |
Reset/verification code |
Passage registers bundled templates as a Leaf source named passage:
// Templates are automatically registered from:
// Sources/Passage/Resources/Views/| Error | Trigger |
|---|---|
Abort(.notFound) |
View not configured but route accessed |
ValidationsError |
Form validation failed |
AbortError |
Business logic error (invalid credentials, etc.) |
The linking views (linkAccountSelect, linkAccountVerify) work with the manual linking flow:
- Select View: Shows list of candidate accounts with masked identifiers
- Verify View: Prompts for password or email code verification
Context includes:
provider: OAuth provider namecandidates: List of matching accounts with masked emails/phonesmaskedEmail/maskedPhone: Partially hidden identifiershasPassword: Whether user can verify via passwordcanUseEmailCode: Whether email verification is available
- Account - Login/registration backend
- Restoration - Password reset backend
- Passwordless - Magic link backend
- Linking - OAuth account linking backend
- Federated Login - OAuth authentication