A dark, terminal-inspired internal developer platform dashboard. A central hub for discovering and accessing all internal tools and services without memorizing URLs.
- App Catalog — Card-based grid of all registered services with search, category filters, and staggered entrance animations
- Favorites — Star/pin apps to float them to the top (persisted in localStorage)
- Recently Visited — Last 5 opened services shown at the top
- Status Indicators — Green/red ping dots showing service reachability
- Copy URL — One-click clipboard copy for any service URL
- Dark/Light Mode — Toggle persisted in localStorage
- Keyboard Navigation — Press
/to focus search,Escapeto clear - Admin Panel — Create, edit, delete and reorder apps at
/admin - Bulk Import — Paste JSON or CSV to import many apps at once
- Export — Download the full app catalog as JSON
- Framework: Next.js 14 (App Router)
- UI: React 18, Tailwind CSS v3, Framer Motion
- State: Zustand with
persistmiddleware - Auth: JWT via
jose(Edge-compatible), HttpOnly cookie; optional OIDC/SSO (Keycloak-compatible) - Data: JSON file store (
data/apps.json) with atomic writes - Icons: Simple Icons CDN
- Fonts: JetBrains Mono + Inter (via
next/font/google) - DnD:
@dnd-kitfor admin drag-and-drop reorder
# 1. Install dependencies
npm install
# 2. Configure environment
cp .env.example .env
# Edit .env — set ADMIN_EMAIL, ADMIN_PASSWORD, and a strong JWT_SECRET
# 3. Start development server
npm run devOpen http://localhost:3000 to view the catalog.
Admin panel: http://localhost:3000/admin
Login: use the credentials from your .env
| Variable | Description | Default |
|---|---|---|
ADMIN_EMAIL |
Admin login email | admin@example.com |
ADMIN_PASSWORD |
Admin login password | changeme |
JWT_SECRET |
JWT signing secret (min 32 chars) | dev-only default |
DATA_FILE_PATH |
Path to apps JSON file | ./data/apps.json |
OIDC_ISSUER |
OIDC provider issuer URL (realm base URL for Keycloak) | — |
OIDC_CLIENT_ID |
Client ID registered in the OIDC provider | — |
OIDC_CLIENT_SECRET |
Client secret (use a confidential client) | — |
OIDC_REDIRECT_URI |
Absolute URL of the callback endpoint | — |
OIDC_ADMIN_GROUP |
Group name that grants admin access (optional — omit to allow any authenticated OIDC user) | — |
OIDC_SCOPES |
Space-separated scopes to request | openid profile email groups |
Generate a strong JWT secret:
openssl rand -base64 32Palantir supports single sign-on via any standard OIDC provider, including Keycloak. When the four required OIDC_* variables are set, a Sign in with SSO button appears on the login page alongside the local credentials form.
- The user clicks Sign in with SSO on the login page.
- Palantir redirects to
/api/auth/oidc/login, which builds an authorization URL with a CSRFstateparameter and redirects to the provider. - After the user authenticates, the provider redirects back to
/api/auth/oidc/callback. - Palantir validates the
state, exchanges the authorization code for tokens, verifies the ID token signature against the provider's JWKS, and extracts the user'sgroupsclaim. - A
palantir_tokensession cookie (JWT, 8-hour TTL, HttpOnly) is issued and the user is redirected to the page they came from.
In the Keycloak Admin Console, create a dedicated realm (e.g. my-realm) or reuse an existing one.
- Navigate to Clients → Create client.
- Set Client ID to
palantir(or any name — this becomesOIDC_CLIENT_ID). - Set Client type to OpenID Connect.
- Enable Client authentication (confidential client).
- Under Valid redirect URIs, add your callback URL:
https://palantir.example.com/api/auth/oidc/callback - Save the client and copy the Client secret from the Credentials tab.
To support group-based admin access (OIDC_ADMIN_GROUP) or team visibility:
- Go to Client scopes → Create client scope.
- Name it
groups, set type to Default, and save. - Inside the new scope, go to Mappers → Add mapper → By configuration → Group Membership.
- Set Token Claim Name to
groupsand enable Add to ID token. - Back on your
palantirclient, go to Client scopes and add thegroupsscope.
Add the following to your .env:
OIDC_ISSUER=https://keycloak.example.com/realms/my-realm
OIDC_CLIENT_ID=palantir
OIDC_CLIENT_SECRET=<client-secret-from-credentials-tab>
OIDC_REDIRECT_URI=https://palantir.example.com/api/auth/oidc/callback
# Optional — restrict admin access to a specific Keycloak group.
# Keycloak prefixes group names with a slash (e.g. /palantir-admins).
# Both "/palantir-admins" and "palantir-admins" are accepted.
OIDC_ADMIN_GROUP=palantir-admins
# Optional — defaults to "openid profile email groups"
OIDC_SCOPES=openid profile email groupsRestart the application. The login page at /login (or /admin/login) should now show a Sign in with SSO button. Clicking it will redirect you through Keycloak and back.
Note: The discovery document at
<OIDC_ISSUER>/.well-known/openid-configurationis fetched at runtime and cached for 1 hour. New signing keys are picked up automatically after the cache expires. During a key rotation, there may be a short window (up to 1 hour) where tokens signed with the new key are rejected until the cache refreshes — plan maintenance windows accordingly.
Apps can be restricted to specific groups. Set the teams field on an app (via the admin UI or apps.json) to an array of group names returned by your OIDC provider:
{
"name": "Internal Metrics",
"teams": ["platform-team", "sre"]
}- Apps with an empty
teamsarray are visible to everyone (including unauthenticated users). - Leading slashes are stripped automatically, so
/platform-teamandplatform-teamare treated as the same group. - Local users (created via the admin panel) can also be assigned to groups, which are matched against the
teamsfield in the same way.
A Helm chart is provided in the helm/palantir/ directory.
# Install with default values (not suitable for production — change credentials!)
helm install palantir ./helm/palantir \
--set env.adminEmail=admin@example.com \
--set env.adminPassword=changeme \
--set env.jwtSecret=$(openssl rand -base64 32)# Generate a strong JWT secret first
JWT_SECRET=$(openssl rand -base64 32)
# Create a values override file
cat > my-values.yaml <<EOF
image:
repository: ghcr.io/themkarimi/palantir
tag: "0.1.0"
env:
adminEmail: "admin@example.com"
adminPassword: "your-secure-password"
jwtSecret: "${JWT_SECRET}"
ingress:
enabled: true
className: nginx
hosts:
- host: palantir.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: palantir-tls
hosts:
- palantir.example.com
EOF
helm install palantir ./helm/palantir -f my-values.yamlenv:
oidc:
issuer: "https://keycloak.example.com/realms/my-realm"
clientId: "palantir"
clientSecret: "your-client-secret"
redirectUri: "https://palantir.example.com/api/auth/oidc/callback"
adminGroup: "palantir-admins" # optional| Value | Description | Default |
|---|---|---|
image.repository |
Container image repository | ghcr.io/themkarimi/palantir |
image.tag |
Image tag (defaults to chart appVersion) | "" |
env.adminEmail |
Admin login email | admin@example.com |
env.adminPassword |
Admin login password (stored in Secret) | changeme |
env.jwtSecret |
JWT signing secret, min 32 chars (stored in Secret) | random |
env.dataFilePath |
Path to apps JSON file inside the container | /app/data/apps.json |
env.oidc.* |
OIDC configuration (all optional) | "" |
existingSecret |
Name of a pre-created Secret with ADMIN_PASSWORD, JWT_SECRET |
"" |
persistence.enabled |
Enable persistent storage for apps.json |
true |
persistence.size |
PVC size | 1Gi |
ingress.enabled |
Create an Ingress resource | false |
autoscaling.enabled |
Enable HorizontalPodAutoscaler | false |
# Build and run with Docker Compose
cp .env.example .env
# Edit .env with production values
docker-compose up -dThe data/ directory is mounted as a volume so your app catalog persists across container restarts.
For standalone Docker builds, set DOCKER_OUTPUT=1 during npm run build.
{
"id": "uuid",
"name": "GitLab",
"url": "https://gitlab.internal.company.com",
"description": "Source code management and CI/CD pipelines",
"category": "CI/CD",
"iconSlug": "gitlab",
"customLogoUrl": null,
"accentColor": "#e24329",
"healthCheckUrl": null,
"teams": [],
"order": 0,
"createdAt": "2024-01-01T00:00:00Z"
}Categories: CI/CD, Monitoring, Databases, Security, Messaging, Storage, Tracing, Code Quality, Registry
Icon slugs: Find valid slugs at simpleicons.org. The slug is used in https://cdn.simpleicons.org/<slug>.
teams: Array of group/team names. Apps with an empty array are visible to all users. When using OIDC, these names are matched against the groups claim from your identity provider (leading slashes stripped). When using local accounts, they are matched against the groups assigned to each user.
In the admin panel under the Bulk Import tab, paste JSON or CSV.
JSON format:
[
{
"name": "My Service",
"url": "https://service.internal.company.com",
"description": "What it does",
"category": "CI/CD",
"iconSlug": "github",
"accentColor": "#24292e"
}
]CSV format:
name,url,description,category,iconSlug,accentColor
My Service,https://service.internal.com,What it does,CI/CD,github,#24292eAll write endpoints require admin authentication (cookie set on login).
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/auth |
— | Login with email/password, set cookie |
DELETE |
/api/auth |
— | Logout, clear cookie |
GET |
/api/auth/oidc/login |
— | Initiate OIDC authorization flow |
GET |
/api/auth/oidc/callback |
— | OIDC callback — exchange code, set cookie |
GET |
/api/apps |
— | List all apps |
POST |
/api/apps |
✓ | Create app |
GET |
/api/apps/:id |
— | Get app |
PUT |
/api/apps/:id |
✓ | Update app |
DELETE |
/api/apps/:id |
✓ | Delete app |
PATCH |
/api/apps/reorder |
✓ | Bulk reorder by { ids: string[] } |
POST |
/api/apps/import |
✓ | Bulk import { data, format } |
GET |
/api/users |
✓ | List local users |
POST |
/api/users |
✓ | Create local user |
PUT |
/api/users/:id |
✓ | Update local user |
DELETE |
/api/users/:id |
✓ | Delete local user |
GET |
/api/groups |
✓ | List groups |
POST |
/api/groups |
✓ | Create group |
PUT |
/api/groups/:id |
✓ | Update group |
DELETE |
/api/groups/:id |
✓ | Delete group |
The green/red dot on each card uses a HEAD request with mode: no-cors to check reachability. Due to browser CORS restrictions, this is a best-effort indicator. For accurate health checks, configure a healthCheckUrl that returns a CORS-friendly response.