Files
cameleer-saas/docs/superpowers/specs/2026-04-06-custom-sign-in-ui-design.md
hsiegeln df220bc5f3
All checks were successful
CI / build (push) Successful in 40s
CI / docker (push) Successful in 50s
feat: custom Logto sign-in UI with Cameleer branding
Replace Logto's default sign-in page with a custom React SPA that
matches the cameleer3-server login page using @cameleer/design-system.

- New Vite+React app at ui/sign-in/ with Experience API integration
- 4-step auth flow: init → verify password → identify → submit
- Design-system components: Card, Input, Button, FormField, Alert
- Same witty random subtitles as cameleer3-server LoginPage
- Dockerfile: add sign-in-frontend build stage, copy dist to image
- docker-compose: CUSTOM_UI_PATH on Logto, shared signinui volume
- SaaS entrypoint copies sign-in dist to shared volume on startup
- Add .gitattributes for LF line endings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:43:22 +02:00

3.9 KiB

Custom Logto Sign-In UI

Problem

Logto's default sign-in page uses Logto branding. While we configured colors and logos via PATCH /api/sign-in-exp, control over layout, typography, and components is limited. The sign-in experience is visually inconsistent with the cameleer3-server login page.

Goal

Replace Logto's sign-in UI with a custom React SPA that visually matches the cameleer3-server login page, using @cameleer/design-system components for consistency across all deployment models.

Scope

MVP: Username/password sign-in only. Later: Sign-up, forgot password, social login, MFA.

Architecture

Source

Separate Vite+React app at ui/sign-in/. Own package.json, shares @cameleer/design-system v0.1.31.

Build

Added as a Docker build stage in the SaaS Dockerfile. The dist is copied into the SaaS image at /app/sign-in-dist/.

Deploy

SaaS entrypoint (docker/saas-entrypoint.sh) copies the built sign-in dist to a shared Docker volume (signinui). Logto reads from that volume via CUSTOM_UI_PATH=/etc/logto/custom-ui.

Flow

User visits /platform/ → SaaS redirects to Logto OIDC
→ Logto sets interaction cookie, serves custom sign-in UI
→ User enters credentials → Experience API 4-step flow
→ Logto redirects back to /platform/callback with auth code

Experience API

The custom UI communicates with Logto via the Experience API (stateful, cookie-based):

1. PUT  /api/experience                      → 204  (init SignIn)
2. POST /api/experience/verification/password → 200  { verificationId }
3. POST /api/experience/identification       → 204  (confirm identity)
4. POST /api/experience/submit               → 200  { redirectTo }

The interaction cookie is set by /oidc/auth before the user lands on the sign-in page. All API calls use credentials: "same-origin".

Visual Design

Matches cameleer3-server LoginPage exactly:

  • Centered Card (400px max-width, 32px padding)
  • Logo: favicon.svg + "cameleer3" text (24px bold)
  • Random witty subtitle (13px muted)
  • FormField + Input for username and password
  • Amber Button (primary variant, full-width)
  • Alert for errors
  • Background: var(--bg-base) (light beige)
  • Dark mode support via design-system tokens

Files

New

File Purpose
ui/sign-in/package.json React 19 + @cameleer/design-system + Vite 6
ui/sign-in/.npmrc Gitea npm registry for @cameleer scope
ui/sign-in/tsconfig.json TypeScript config
ui/sign-in/vite.config.ts Vite build config
ui/sign-in/index.html Entry HTML
ui/sign-in/src/main.tsx React mount + design-system CSS import
ui/sign-in/src/App.tsx App shell
ui/sign-in/src/SignInPage.tsx Login form with Experience API integration
ui/sign-in/src/SignInPage.module.css CSS Modules (matches server LoginPage)
ui/sign-in/src/experience-api.ts Typed Experience API client
docker/saas-entrypoint.sh Copies sign-in dist to shared volume
.gitattributes LF line endings

Modified

File Change
Dockerfile Add sign-in-frontend build stage, copy dist to runtime image
docker-compose.yml Add CUSTOM_UI_PATH to Logto, signinui shared volume

Docker Timing

Logto starts before SaaS (dependency chain: postgres → logto → logto-bootstrap → cameleer-saas). The CUSTOM_UI_PATH volume is initially empty. This is acceptable because:

  1. Nobody hits the sign-in page until redirected from /platform/
  2. By that time, SaaS has started and copied the dist files
  3. Logto serves static files at request time, not startup time

Future Extensions

  • Add React Router for multiple pages (sign-up, forgot password)
  • Fetch GET /api/.well-known/sign-in-exp for dynamic sign-in method detection
  • Social login buttons via POST /api/experience/verification/social
  • MFA via POST /api/experience/verification/totp
  • Consent page via GET /api/interaction/consent