This is a comprehensive admin panel for managing Traefik dynamic configurations with authentication support including shared links and SSO integration.
Purpose: Provides a web-based admin interface for dynamically configuring Traefik reverse proxy services with authentication and session management.
Tech Stack:
- Next.js 15 with App Router and TypeScript
- Drizzle ORM with PostgreSQL database
- shadcn/ui component library
- Tailwind CSS with dark mode support
- Docker Compose for PostgreSQL
- CRUD operations for proxy services (IP, port, subdomain configuration)
- Real-time Traefik configuration generation via HTTP provider
- Configurable global domain settings (e.g.,
exposed.example.comwhere services becomesubdomain.basedomain) - Wildcard certificate support to prevent service name leakage in Certificate Transparency logs
- None: Public access without authentication
- Shared Links: Time-limited, one-use links with configurable session duration
- SSO Integration: OAuth2/OIDC with group/user authorization
- Memory-cached sessions with database persistence for performance
- Admin interface for viewing and managing active sessions
- Real-time session validation for Traefik forward-auth
- Automatic cleanup of expired sessions
- Configurable base domain for all services
- Certificate resolver configuration for DNS challenge mode
- Global and per-service middleware management with proper ordering
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Admin Panel │────▶│ PostgreSQL │ │ Traefik │
│ (Next.js) │ │ Database │ │ Reverse │
└─────────────────┘ └─────────────────┘ │ Proxy │
└─────────────────┘
│
┌───────────────────────────┘
▼
┌─────────────────┐
│ Target Services │
│ (HTTP/HTTPS) │
└─────────────────┘
- services: Service configurations with auth methods and middleware settings
- shared_links: Time-limited authentication links
- sessions: Active user sessions with memory caching
- app_config: Global application configuration
GET /api/traefik/config- Dynamic Traefik configuration endpoint
GET /api/services- List all servicesPOST /api/services- Create new servicePUT /api/services/[id]- Update serviceDELETE /api/services/[id]- Delete service
GET /api/auth/verify- Forward-auth endpoint for TraefikGET /api/sessions- List active sessionsDELETE /api/sessions- Delete all sessions
GET /api/config- Get global Traefik configurationPUT /api/config- Update global configuration
# Database operations
pnpm db:generate # Generate new migration
pnpm db:push # Push schema changes
pnpm db:studio # View database in Drizzle Studio
# Development
pnpm dev # Start development server
pnpm build # Build for production
pnpm lint # Run lintinglib/traefik-config.ts- Traefik configuration generation logiclib/app-config.ts- Global configuration managementlib/session-manager.ts- Session management with memory cachingdb/schema.ts- Database schema definitions
app/api/traefik/config/route.ts- Traefik configuration endpointapp/api/services/- Service CRUD operationsapp/api/auth/verify/route.ts- Forward-auth validationapp/api/config/route.ts- Global configuration management
app/page.tsx- Main admin panel with service managementapp/config/page.tsx- Global configuration pageapp/sessions/page.tsx- Session management interfacecomponents/confirm-dialog.tsx- Reusable confirmation dialogscomponents/unsaved-changes-guard.tsx- Unsaved changes protection
- All authentication tokens stored securely with httpOnly cookies
- CSRF protection through state parameters in SSO flows
- Session tokens are cryptographically secure random values
- Forward-auth validation prevents unauthorized access
- Automatic session cleanup prevents token accumulation
The application requires:
- PostgreSQL database
- Environment variables for database connection and SSO configuration
- Traefik configured to use the HTTP provider endpoint
See README.md for detailed setup instructions.
{
"certResolver": "letsencrypt-dns",
"globalMiddlewares": ["compression", "security-headers", "rate-limit"]
}This creates services accessible as {service}.exposed.example.com with wildcard certificates and standard security middlewares.
providers:
http:
endpoints:
- "http://localhost:3000/api/traefik/config"
pollInterval: "10s"This configures Traefik to poll the admin panel for dynamic configuration updates.
Project uses pnpm instead of npm
This project has encountered recurring issues with form components (especially Select dropdowns) not displaying their selected values correctly. Based on fixes applied in commits 4552b2a (Fix auto disable selection) and 2d2c181 (Host header override), follow these patterns to prevent form state issues:
- Select Components Not Showing Selected Values: Dropdowns appear blank even when form data has correct values
- Race Conditions: Form initializes before data is loaded, causing mismatched state
- Value Type Mismatches: UI expects strings, backend provides null/undefined, causing rendering issues
❌ Wrong:
const defaultFormData: ServiceFormData = {
name: "",
enableDurationMinutes: defaultDuration || null,
domainId: "",
};✅ Correct:
const getDefaultFormData = useCallback((): ServiceFormData => ({
name: "",
enableDurationMinutes: defaultDuration ?? null, // Use ?? instead of ||
domainId: "",
}), [defaultDuration]); // Include dependencies❌ Wrong:
<Select
value={formData.enableDurationMinutes?.toString() || "null"}
onValueChange={(value) => {
const duration = value === "null" ? null : parseInt(value);
setFormData({ ...formData, enableDurationMinutes: duration });
}}
>✅ Correct:
<Select
value={formData.enableDurationMinutes === null || isNaN(formData.enableDurationMinutes as number)
? "forever"
: formData.enableDurationMinutes?.toString() || "forever"}
onValueChange={(value) => {
// Ignore empty string changes - spurious event from Select component
if (value === "") {
return;
}
let duration: number | null;
if (value === "forever") {
duration = null;
} else {
const parsed = parseInt(value);
duration = isNaN(parsed) ? null : parsed;
}
setFormData({ ...formData, enableDurationMinutes: duration });
}}
>❌ Wrong:
<Select
value={formData.domainId}
disabled={submitting}
>✅ Correct:
<Select
value={formData.domainId || ""}
disabled={submitting || domains.length === 0} // Disable until data loads
onValueChange={(value) => {
// Ignore spurious empty string events
if (value === "") {
return;
}
updateFormData({ domainId: value });
}}
>- Always handle empty string events: Select components emit spurious
""values, ignore them - Map null/undefined to UI-friendly strings: Use "forever", "none", etc. for display
- Use
??instead of||: Proper null coalescing prevents0andfalseissues - Disable components during loading: Prevent interaction until required data is available
- Use proper dependencies in useCallback/useEffect: Ensure fresh data when dependencies change
- Handle async timing: Don't assume data is available when components render
Always test these scenarios:
- Fresh page load (new item creation)
- Edit existing items (form population)
- Data loading states (async dependencies)
- Edge cases (null, undefined, empty string values)
Following these patterns will prevent the recurring form state issues this project has experienced.