Files
cameleer-saas/docs/superpowers/specs/2026-04-06-custom-sign-in-ui-design.md
hsiegeln edbb66b056
All checks were successful
CI / build (push) Successful in 1m15s
CI / docker (push) Successful in 2m52s
docs: update architecture for custom sign-in UI and CI pipeline
- CLAUDE.md: add custom sign-in UI section, update routing table,
  document auto-redirect, CI-built images, no local builds, dev
  override without volume mounts
- Design spec: reflect final implementation — custom Logto image,
  no CUSTOM_UI_PATH, no init containers, bundled favicon

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

3.8 KiB

Custom Logto Sign-In UI — IMPLEMENTED

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 (implemented): 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

Custom Logto Docker image (cameleer-logto). ui/sign-in/Dockerfile has a multi-stage build: node stage builds the SPA, then copies dist over Logto's built-in experience directory at /etc/logto/packages/experience/dist/. CI builds and pushes the image.

Note: CUSTOM_UI_PATH env var does NOT work for Logto OSS — it's a Logto Cloud-only feature. The correct approach for self-hosted is replacing the experience dist directory.

Deploy

docker-compose.yml pulls the pre-built cameleer-logto image. No init containers, no shared volumes, no local builds needed.

Auth Flow

User visits /platform/ → LoginPage auto-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
→ SaaS app exchanges code for token, user lands on dashboard

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
  • Favicon bundled in ui/sign-in/public/favicon.svg (served by Logto, not SaaS)

Files

File Purpose
ui/sign-in/Dockerfile Multi-stage: node build + FROM logto:latest + COPY dist
ui/sign-in/package.json React 19 + @cameleer/design-system + Vite 6
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 (4-step flow)
ui/sign-in/src/main.tsx React mount + design-system CSS import
ui/sign-in/public/favicon.svg Cameleer camel logo (bundled in dist)
ui/src/auth/LoginPage.tsx Auto-redirects to Logto OIDC (no button)
.gitea/workflows/ci.yml Builds and pushes cameleer-logto image
docker-compose.yml Uses cameleer-logto image (no build directive)

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