Introduces a complete design dials system that allows designers and PMs to adjust UI parameters at runtime without code changes. **Dials SDK (`packages/dials/`):** - useDynamicColor: Color values with design system integration - useDynamicSpacing: Spacing/padding/margin controls - useDynamicVariant: Discrete choice selections - useDynamicBoolean: Toggle/feature flag controls - useDynamicNumber: Numeric values with min/max/step - DialsOverlay: Compact Leva-inspired UI (Ctrl+D to toggle) - DialsProvider: React context for dial state management - Design manifest integration for design system tokens **App Integration:** - Added DialsProvider to app Providers - Example dials on WebsitePage (metrics bar, panels, navigation) - MetricCard component with adjustable typography dials - TypeScript manifest at src/config/niteshift-manifest.ts **Documentation:** - Comprehensive CLAUDE.md section on dials usage - Best practices for preserving original appearance - Examples for all dial types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
32 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Niteshift Sandbox Status
This repository is running in a Niteshift sandbox with the following already configured:
✅ Dev server is ALREADY RUNNING on http://localhost:3001 with hot reload enabled ✅ Database is set up and migrated (PostgreSQL) ✅ All dependencies are installed (pnpm) ✅ Environment variables are configured (.env file exists) ✅ Tracker script and geo database are built
Default credentials:
- Username:
admin - Password:
umami
You can start making changes immediately - all changes will hot reload automatically!
TL;DR - Start Prototyping Now
// 1. Access the running app
// http://localhost:3001 (login: admin/umami)
// 2. Make a quick UI change - edit any file in src/
import { Button } from '@umami/react-zen'; // Use component library
import { useApi } from '@/components/hooks'; // Fetch data with React Query
// 3. Save file - see changes instantly in browser!
Most common tasks:
- New page: Create
src/app/(main)/[feature]/page.tsx - New component: Add to
src/components/common/ - Fetch data: Use
useApi()hook or pre-built query hooks fromsrc/components/hooks/queries/ - Styles: Use
@umami/react-zencomponents (minimal custom CSS needed)
Overview
Umami is a privacy-focused web analytics platform built with Next.js 15, TypeScript, and PostgreSQL. It serves as an alternative to Google Analytics with a focus on simplicity and privacy.
Tech Stack
- Framework: Next.js 15 with App Router
- Language: TypeScript (ES2022 target)
- Database: PostgreSQL with Prisma ORM (supports optional ClickHouse for analytics)
- Package Manager: pnpm
- UI Components: React 19 with
@umami/react-zencomponent library - Data Fetching:
@tanstack/react-query(React Query) - State Management: Zustand stores
- Styling: Minimal custom CSS (CSS Modules with PostCSS), primarily using component library
- Internationalization:
react-intlwith customuseMessageshook - Charts: Chart.js with react-spring animations
- Build Tools: Rollup (for tracker), Next.js with Turbo
- Testing: Jest with ts-jest
Useful Commands (Niteshift Sandbox)
Note: The dev server is already running - you don't need to start it!
Testing
pnpm test # Run Jest tests
Code Quality
pnpm lint # Run ESLint (optional - runs automatically on commit)
Database (if schema changes needed)
pnpm build-db-client # Regenerate Prisma client after schema changes
pnpm update-db # Apply new migrations
If You Need to Rebuild Assets
pnpm build-tracker # Rebuild tracking script (rarely needed)
pnpm build-geo # Rebuild geo database (rarely needed)
Rapid UI Prototyping Guide
Quick Start for UI Changes
The dev server is already running on http://localhost:3001 with hot reload!
-
Make your changes - Edit any file in
src/ -
View instantly - Changes appear in browser automatically (no manual refresh needed)
-
Component library: Use
@umami/react-zenfor all UI primitives:import { Button, Form, TextField, Modal, Row, Column } from '@umami/react-zen'; -
Import path alias: Always use
@/for imports:import { useApi } from '@/components/hooks'; import { formatDate } from '@/lib/date';
UI Component Architecture
Component Library (@umami/react-zen)
The codebase uses a custom component library for UI primitives. Common components include:
- Layout:
Row,Column,Container,Grid - Forms:
Form,TextField,TextArea,Select,Checkbox,Toggle,SearchField - Buttons:
Button,ActionButton,IconButton - Display:
Modal,Dropdown,Menu,Tabs,Banner,Tooltip - Data:
Table,List,Empty
Example component usage:
import { Button, Form, TextField } from '@umami/react-zen';
export function MyForm() {
return (
<Form>
<TextField name="email" placeholder="Email" />
<Button type="submit">Submit</Button>
</Form>
);
}
Custom Components (@/components/common/)
Pages typically use a mix of @umami/react-zen components and custom components from @/components/common/. Common custom components include:
- Layout:
PageBody,PageHeader,Panel,SectionHeader - Data Display:
DataGrid,Empty,EmptyPlaceholder,LoadingPanel - Forms:
ActionForm,ConfirmationForm,TypeConfirmationForm - UI Elements:
Avatar,Favicon,FilterLink,LinkButton,Pager - Utilities:
DateDisplay,DateDistance,ErrorMessage,ExternalLink
Example: Most pages combine both:
import { Column } from '@umami/react-zen';
import { PageBody, PageHeader, Panel } from '@/components/common';
export function MyPage() {
return (
<PageBody>
<Column margin="2">
<PageHeader title="My Page" />
<Panel>
{/* Content */}
</Panel>
</Column>
</PageBody>
);
}
Other Component Locations
src/components/input/: Input/button components (DateFilter, DownloadButton, WebsiteSelect, etc.)src/components/charts/: Chart components (BarChart, LineChart, etc.)src/components/metrics/: Analytics displays (MetricCard, StatsTable, etc.)src/components/boards/: Dashboard board components
Data Fetching Patterns
Pre-built Query Hooks (Recommended)
Most common pattern: Use existing query hooks from src/components/hooks/queries/. See src/components/hooks/index.ts for the full list of available hooks.
import { useWebsitesQuery } from '@/components/hooks/queries/useWebsitesQuery';
import { useReportsQuery } from '@/components/hooks/queries/useReportsQuery';
import { useUsersQuery } from '@/components/hooks/queries/useUsersQuery';
// In component:
const { data: websites, isLoading } = useWebsitesQuery();
Available query hooks include: useWebsitesQuery, useUsersQuery, useReportsQuery, useTeamsQuery, useLinksQuery, usePixelsQuery, useRealtimeQuery, useWebsiteMetricsQuery, useWebsiteEventsQuery, useSessionDataQuery, and many more. Check src/components/hooks/queries/ for the complete list.
Using the useApi Hook Directly
For custom API calls or when a pre-built hook doesn't exist, use useApi():
import { useApi } from '@/components/hooks';
import { useQuery, useMutation } from '@tanstack/react-query';
export function MyComponent() {
const { get, post } = useApi();
// Option 1: Use with React Query hooks directly
const { data, isLoading, error } = useQuery({
queryKey: ['custom-data'],
queryFn: () => get('/custom-endpoint'),
});
// Option 2: Direct API call (for mutations, form submissions, etc.)
const handleSubmit = async (values) => {
await post('/api/resource', values);
};
return <div>{data?.name}</div>;
}
Note: Most query hooks internally use useApi() with usePagedQuery() for pagination support. Prefer pre-built hooks when available.
State Management
Zustand Stores
Global state is managed with Zustand stores in src/store/:
import { useApp } from '@/store/app';
import { setLocale, setTimezone, setUser } from '@/store/app';
// In component - read state
const user = useApp(state => state.user);
const locale = useApp(state => state.locale);
// Update state
setUser(userData);
setLocale('en-US');
Available stores:
app.ts: Global app state (user, locale, theme, timezone, dateRange)dashboard.ts: Dashboard statewebsites.ts: Websites statecache.ts: Cache managementversion.ts: Version checking
Custom Hooks
Essential hooks in src/components/hooks/:
Data Hooks
useApi(): API wrapper with React Query integrationuseConfig(): Get app configurationuseDateRange(): Date range managementuseFilters(): Filter state managementusePagedQuery(): Pagination helper
UI Hooks
useMessages(): Internationalization (i18n) messages and formattinguseLocale(): Current localeuseFormat(): Number, date, and value formattinguseMobile(): Mobile device detectionuseNavigation(): Next.js navigation helpers (see below for detailed usage)useEscapeKey(): ESC key handleruseDocumentClick(): Click outside handler
useNavigation() Hook Details
The useNavigation() hook is essential for handling URL query parameters and navigation in the app:
import { useNavigation } from '@/components/hooks';
export function MyComponent() {
const { router, pathname, query, updateParams, replaceParams } = useNavigation();
// updateParams() - Merges new params with existing ones, returns URL
const newUrl = updateParams({ country: 'eq.US' }); // Keeps other filters
// IMPORTANT: To actually navigate, use router.replace() with the URL:
const handleFilterClick = (countryCode) => {
router.replace(updateParams({ country: `eq.${countryCode}` }));
};
// replaceParams() - Replaces all params, returns URL
const resetUrl = replaceParams({ page: 1 }); // Only page param
// Access current query params
console.log(query); // { country: 'eq.US', browser: 'eq.Chrome' }
}
Key points:
updateParams()returns a URL string - it doesn't navigate automatically- Always call
router.replace()with the URL to actually navigate - Use
router.replace()instead ofrouter.push()to replace the current history entry updateParams()preserves existing filters,replaceParams()clears them
Context Hooks
These hooks provide access to React Context values for specific entities:
useUser(): Access current user context (fromUserProvider)useWebsite(): Access current website context (fromWebsiteProvider)useTeam(): Access current team context (fromTeamProvider)useLink(): Access current link context (fromLinkProvider)usePixel(): Access current pixel context (fromPixelProvider)
Usage: These hooks are typically used within components that are wrapped by their respective providers. For example:
import { useWebsite } from '@/components/hooks';
export function MyComponent() {
const website = useWebsite(); // Returns website data from WebsiteProvider
return <div>{website?.name}</div>;
}
Example Usage:
import { useMessages, useFormat, useMobile } from '@/components/hooks';
export function MyComponent() {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const isMobile = useMobile();
return (
<div>
{formatMessage(labels.welcome)}
{formatValue(1000, 'number')}
</div>
);
}
Internationalization (i18n)
All user-facing text should use useMessages:
import { useMessages } from '@/components/hooks';
export function MyComponent() {
const { formatMessage, labels } = useMessages();
return (
<div>
<h1>{formatMessage(labels.dashboard)}</h1>
<p>{formatMessage(labels.description)}</p>
</div>
);
}
Messages are defined in src/components/messages.ts and compiled to JSON files in public/intl/messages/.
Styling
Approach
- Minimal custom CSS: The codebase relies heavily on
@umami/react-zencomponents for styling - CSS Modules: Use when custom styles are needed (
.module.cssfiles) - Global styles:
src/styles/global.cssandsrc/styles/variables.css - CSS Variables: Primary color customization via
--primary-color
Adding Custom Styles
Only create CSS modules when absolutely necessary:
// MyComponent.module.css
import styles from './MyComponent.module.css';
export function MyComponent() {
return <div className={styles.container}>Content</div>;
}
Common UI Patterns
DataGrid Pattern
For lists with search, pagination, and actions:
import { DataGrid } from '@/components/common/DataGrid';
import { useWebsitesQuery } from '@/components/hooks/queries/useWebsitesQuery';
export function WebsiteList() {
const query = useWebsitesQuery();
return (
<DataGrid
query={query}
allowSearch
allowPaging
renderActions={() => <Button>Add Website</Button>}
>
{(row) => <div>{row.name}</div>}
</DataGrid>
);
}
Form Pattern
Standard form with validation:
import { Form, TextField, Button } from '@umami/react-zen';
import { useApi } from '@/components/hooks';
export function MyForm() {
const { post } = useApi();
const handleSubmit = async (values) => {
await post('/api/resource', values);
};
return (
<Form onSubmit={handleSubmit}>
<TextField name="name" label="Name" required />
<Button type="submit">Submit</Button>
</Form>
);
}
Modal Pattern
import { Modal, Button } from '@umami/react-zen';
import { useState } from 'react';
export function MyComponent() {
const [showModal, setShowModal] = useState(false);
return (
<>
<Button onClick={() => setShowModal(true)}>Open</Button>
{showModal && (
<Modal onClose={() => setShowModal(false)}>
<div>Modal content</div>
</Modal>
)}
</>
);
}
Page Structure
Pages in src/app/(main)/ follow this pattern:
'use client'; // Required for interactive components
import { useMessages } from '@/components/hooks';
import { Button } from '@umami/react-zen';
export default function MyPage() {
const { formatMessage, labels } = useMessages();
return (
<div>
<h1>{formatMessage(labels.title)}</h1>
{/* Page content */}
</div>
);
}
To view your new page: Navigate to http://localhost:3001/your-route (server is already running!)
Tips for Rapid Prototyping in Niteshift
- Zero setup needed: The dev server is running, database is ready, just start coding!
- Instant feedback: Hot reload is active - save a file and see changes in <1 second
- Leverage existing components: Check
src/components/common/andsrc/components/input/before creating new components - Use query hooks: Don't write custom API calls if a query hook exists in
src/components/hooks/queries/ - Follow established patterns: Look at similar pages/components for consistent patterns
- Internationalization: Always use
useMessages()for text, never hardcode strings - TypeScript: Leverage type inference - the codebase has strong typing
- Component library first: Always check if
@umami/react-zenhas what you need before writing custom UI
Niteshift Dials SDK
New capability for design prototyping! The Dials SDK allows you to expose design parameters as runtime-adjustable "dials" that users can tweak via an overlay UI. This gives designers and PMs fine-grained control without requiring code changes.
Setup and Installation
The Dials SDK lives in packages/dials/ and is automatically built when you run pnpm install via the repo’s postinstall script. Do not edit the SDK source or build pipeline unless the user explicitly instructs you to do so. Your default interaction with dials should be importing the provided hooks inside app components.
Key files:
packages/dials/src/– SDK source (hands-off unless asked)packages/dials/dist/– Built package (gitignored, auto-generated)src/config/niteshift-manifest.ts– Umami’s design system manifest for reference in app code
If—and only if—a user asks you to modify the SDK internals, rebuild with:
pnpm --filter @niteshift/dials build
When to Use Dials
Only add dials when the user explicitly says they want “dials” (or otherwise asks for adjustable controls). When they do, prioritize these scenarios:
- Subjective color/spacing/typography tweaks tied to design-system tokens
- Layout variants or experimental sections the user wants to tune live
- Feature toggles the user specifically calls out for dial-based control
Key principle: Follow the user's direction—never invent dials on your own; create them only when requested, and let the user decide which values need adjustment.
Critical: Preserving Original Appearance
⚠️ EXTREMELY IMPORTANT: When adding dials to existing code, the defaults MUST ALWAYS preserve the exact original appearance.
Before adding dials to any component:
- Document the original values - Record what props/styles existed before dials
- Match defaults exactly - Dial defaults must produce identical output to pre-dial code
- Use empty string for "no prop" - If original had no prop, use
default: ''not'inherit'or a value - Conditionally spread props - Only pass props when they have truthy values
Example - WRONG approach:
// Original code (before dials):
<Text weight="bold">{label}</Text> // No size prop!
// ❌ WRONG - adds size prop that wasn't there:
const labelSize = useDynamicVariant('label-size', {
default: '1', // ❌ Original had NO size, this changes appearance!
options: ['0', '1', '2', '3'] as const,
});
<Text size={labelSize} weight="bold">{label}</Text>
Example - CORRECT approach:
// Original code (before dials):
<Text weight="bold">{label}</Text> // No size prop!
// ✅ CORRECT - empty string means "no change":
const labelSize = useDynamicVariant('label-size', {
default: '', // ✅ Empty string = no size prop = matches original
options: ['', '0', '1', '2', '3'] as const, // First option is "default/none"
});
// ✅ CORRECT - only pass size if truthy:
<Text
{...(labelSize && { size: labelSize })}
weight="bold"
>
{label}
</Text>
Why this matters:
- Users expect dials at default = original appearance
- Dials should enable exploration, not force changes
- Breaking the original look confuses users and defeats the purpose
Testing your defaults:
- Add dials with defaults
- View the page - should look IDENTICAL to before dials
- Reset All in dials overlay - should look IDENTICAL to before dials
- Only when adjusting dials should appearance change
Design System Manifest
The design system manifest is defined in src/config/niteshift-manifest.ts as a TypeScript module:
- Colors (primary, base, accent, semantic)
- Spacing scale (4px to 128px)
- Typography (fonts, sizes, weights)
- Border radius, shadows
Benefits of TypeScript manifest:
- Type-safe with full IDE autocomplete
- Bundled with app (not publicly accessible)
- No runtime HTTP fetch (faster)
- Hot reload compatible
Reference these tokens in dial configs to provide users with design system-aligned options.
Manifest-Powered Defaults
Smart defaults from the design system! When you omit the options parameter in color and spacing dials, the SDK automatically pulls values from the design manifest. This reduces boilerplate and ensures consistency with your design system.
Color dials (design system defaults)
import { useDynamicColor } from '@niteshift/dials';
const badgeColor = useDynamicColor('hero-badge-color', {
label: 'Hero Badge Color',
group: 'Hero Section',
default: 'var(--primary-color)',
manifestCategory: 'primary', // pulls tokens from designManifest.colors.primary
allowCustom: true,
});
return <Badge style={{ backgroundColor: badgeColor }}>Top Performer</Badge>;
Spacing dials (design system defaults)
import { useDynamicSpacing } from '@niteshift/dials';
const cardPadding = useDynamicSpacing('hero-card-padding', {
label: 'Hero Card Padding',
group: 'Hero Section',
default: 'var(--spacing-5)',
manifestCategory: 'spacing',
});
return <Card style={{ padding: cardPadding }}>{children}</Card>;
// Manifest defaults (automatic - uses full spacing scale): const margin2 = useDynamicSpacing('margin-2', { label: 'Margin', default: '24px', // options omitted - uses designManifest.spacing.values (4px to 128px) });
**When to use manifest defaults:**
- ✅ You want design system consistency
- ✅ You're prototyping and want quick setup
- ✅ The default color category (accent) or spacing scale fits your needs
- ❌ You need a specific subset of values
- ❌ You're using custom values outside the design system
### Available Dial Types
#### Color Dials
For any color value (backgrounds, text, borders, etc.):
```typescript
import { useDynamicColor } from '@niteshift/dials';
const bgColor = useDynamicColor('hero-background', {
label: 'Hero Background Color',
description: 'Background color for the hero section',
group: 'Hero Section',
default: '#1a1a1a',
options: ['#1a1a1a', '#2d2d2d', '#404040', '#525252'], // From design system
allowCustom: true // Allows custom hex input
});
<div style={{ backgroundColor: bgColor }}>...</div>
Spacing Dials
For padding, margin, gap, dimensions:
import { useDynamicSpacing } from '@niteshift/dials';
const padding = useDynamicSpacing('card-padding', {
label: 'Card Padding',
group: 'Card Component',
default: '1.5rem',
options: ['0.5rem', '1rem', '1.5rem', '2rem', '3rem'],
allowCustom: true
});
<div style={{ padding }}>...</div>
Variant Dials
For discrete choices (layouts, styles, chart types):
import { useDynamicVariant } from '@niteshift/dials';
const layout = useDynamicVariant('dashboard-layout', {
label: 'Dashboard Layout',
group: 'Dashboard',
default: 'grid',
options: ['grid', 'list', 'compact'] as const
});
{layout === 'grid' && <GridView />}
{layout === 'list' && <ListView />}
{layout === 'compact' && <CompactView />}
Boolean Dials
For toggles, feature flags, show/hide:
import { useDynamicBoolean } from '@niteshift/dials';
const showDelta = useDynamicBoolean('show-metrics-delta', {
label: 'Show Change Indicators',
description: 'Display +/- changes in metrics',
default: true,
trueLabel: 'Visible',
falseLabel: 'Hidden',
group: 'Metrics Bar'
});
{showDelta && <DeltaIndicator value={change} />}
Number Dials
For numeric values with constraints:
import { useDynamicNumber } from '@niteshift/dials';
const chartHeight = useDynamicNumber('chart-height', {
label: 'Chart Height',
default: 400,
min: 200,
max: 800,
step: 50,
unit: 'px',
options: [300, 400, 500, 600], // Preset options
group: 'Chart'
});
<Chart height={chartHeight} />
Advanced Use Cases
Layout Controls:
const columns = useDynamicVariant('metrics-columns', {
label: 'Metrics Layout',
default: '4',
options: ['2', '3', '4', '6'] as const,
group: 'Dashboard'
});
<Grid columns={Number(columns)}>
{metrics.map(m => <MetricCard key={m.id} {...m} />)}
</Grid>
Chart Type Selection:
const chartType = useDynamicVariant('analytics-chart', {
label: 'Visualization Type',
default: 'line',
options: ['line', 'bar', 'area'] as const,
group: 'Analytics'
});
{chartType === 'line' && <LineChart data={data} />}
{chartType === 'bar' && <BarChart data={data} />}
{chartType === 'area' && <AreaChart data={data} />}
Feature Flags:
const showSparklines = useDynamicBoolean('show-sparklines', {
label: 'Show Sparklines',
default: false,
group: 'Metrics Display'
});
<MetricsBar data={metrics} showSparklines={showSparklines} />
Icon Selection:
const emptyIcon = useDynamicVariant('empty-state-icon', {
label: 'Empty State Icon',
default: 'inbox',
options: ['inbox', 'folder', 'archive', 'alert'] as const,
group: 'Empty States'
});
const icons = { inbox: <Inbox />, folder: <Folder />, archive: <Archive />, alert: <Alert /> };
<EmptyState icon={icons[emptyIcon]} />
Best Practices
- Use semantic IDs: e.g.,
'hero-background'not'color-1' - Provide design system options first: Always include tokens from
.niteshift-manifest - Group related dials: Use the
groupproperty to organize by component/section - Add helpful labels: Make labels clear for non-technical users
- Set sensible defaults: Choose the best option; users can refine later
- Reference manifest colors: Pull from design system categories:
// Colors from .niteshift-manifest options: ['#147af3', '#2680eb', '#0090ff', '#3e63dd'] // Primary colors
Communicating with Users
After creating dials, tell the user:
"I've made [X, Y, Z] adjustable via design dials. Press Ctrl+D on macOS (use the Control key, not Command) or Ctrl+Alt+D on Windows/Linux to open the panel and fine-tune these values. You can select from design system options or enter custom values."
Accessing the Overlay
- Keyboard shortcut:
Ctrl+Don macOS,Ctrl+Alt+Don Windows/Linux toggles the dials overlay - Location: Bottom-left floating panel
- Persistence: Visibility state and dial values persist across reloads (localStorage)
- Features:
- Search/filter dials
- Grouped by component/section
- Reset individual dials or all at once
- Keyboard shortcut hint shown in overlay header
Examples from Umami
WebsitePage with dynamic chart:
const chartHeight = useDynamicNumber('website-chart-height', {
label: 'Chart Height',
default: 520,
options: [400, 520, 640, 760],
allowCustom: true,
unit: 'px',
group: 'Website Analytics'
});
<Panel minHeight={`${chartHeight}px`}>
<WebsiteChart websiteId={websiteId} />
</Panel>
Dashboard with layout options:
const layout = useDynamicVariant('dashboard-layout', {
label: 'Board Layout',
default: 'grid',
options: ['grid', 'list', 'masonry'] as const,
group: 'Dashboard'
});
{layout === 'grid' && <GridLayout boards={boards} />}
{layout === 'list' && <ListView boards={boards} />}
{layout === 'masonry' && <MasonryLayout boards={boards} />}
Implementation Notes
- Dials are already integrated into the app via
DialsProviderinsrc/app/Providers.tsx - The overlay (
DialsOverlay) is automatically rendered - Values are persisted to localStorage and survive hot reloads
- No additional setup required - just import and use the hooks!
Project Architecture
Directory Structure
-
src/app/: Next.js App Router pages and API routessrc/app/(main)/: Main application pages (authenticated area)src/app/(collect)/: Analytics data collection endpointssrc/app/api/: REST API routes (admin, auth, teams, websites, reports, etc.)src/app/login/,src/app/logout/,src/app/sso/: Authentication pagessrc/app/share/: Public sharing pages
-
src/lib/: Core utility functions and shared logic- Database utilities (
db.ts) - Authentication (
auth.ts) - Request/response helpers (
request.ts,response.ts) - Date utilities (
date.ts) - Constants (
constants.ts) - IP detection (
ip.ts) - Device detection (
detect.ts) - Formatting utilities (
format.ts)
- Database utilities (
-
src/queries/: Database query layersrc/queries/prisma/: Prisma queries for PostgreSQLsrc/queries/sql/: Raw SQL queries (organized by feature: events, pageviews, sessions, reports)
-
src/components/: React componentssrc/components/common/: Shared UI componentssrc/components/input/: Form input componentssrc/components/charts/: Chart componentssrc/components/metrics/: Analytics metric displayssrc/components/boards/: Dashboard boardssrc/components/hooks/: Custom React hookssrc/components/svg/: SVG icon components (generated bypnpm build-icons)
-
src/tracker/: Client-side tracking script source code -
src/permissions/: Authorization and permission checks -
src/store/: Zustand state management stores -
src/generated/prisma/: Auto-generated Prisma client (DO NOT edit manually) -
prisma/schema.prisma: Prisma schema definition -
scripts/: Build and utility scriptscheck-db.js: Database connection verificationbuild-geo.js: GeoIP database setup
API Architecture
API routes follow Next.js App Router conventions with route.ts files. Standard pattern:
- Request parsing with
parseRequest()from@/lib/request - Authentication via
checkAuth()(can be skipped withskipAuth: true) - Schema validation using Zod
- Database queries via Prisma or raw SQL
- Response formatting via helpers from
@/lib/response
Example:
export async function GET(request: Request) {
const { query, auth, error } = await parseRequest(request, schema);
if (error) return error();
// ... business logic
return json(data);
}
Database Query Pattern
The codebase supports multiple database backends through runQuery() in src/lib/db.ts:
- Prisma queries (in
src/queries/prisma/): Used with PostgreSQL - Raw SQL queries (in
src/queries/sql/): Direct SQL for complex analytics - ClickHouse queries: Optional high-performance analytics backend
Use runQuery() with object containing query implementations for each backend:
return runQuery({
[PRISMA]: () => prismaQuery(),
[CLICKHOUSE]: () => clickhouseQuery(),
});
Environment Variables
In Niteshift: The .env file is already configured with DATABASE_URL and any required variables.
See next.config.ts for full list of available environment variables.
Key Data Models
Core entities in prisma/schema.prisma:
- User: User accounts with authentication
- Team: Organizations/teams
- Website: Tracked websites
- Session: User sessions with device/location data
- WebsiteEvent: Individual events/pageviews
- Report: Custom analytics reports
- Link: Short links (link tracking feature)
- Pixel: Tracking pixels
Important Notes for Niteshift Development
TypeScript Configuration
- Module resolution:
bundler - Path alias:
@/*maps tosrc/* - Strict mode enabled (except
strictNullChecks) - Target: ES2022
- Types are checked automatically - errors show in editor
Testing
- Unit tests in
src/lib/__tests__/using Jest - Test pattern:
*.test.tsor*.spec.ts - Run with
pnpm test
Code Style
- Prettier and ESLint run automatically via pre-commit hooks
- Manually run with
pnpm lintif needed - Stylelint for CSS validation
Database & Prisma
- Prisma client is in
src/generated/prisma/(not default location) - Never edit generated files manually
- If you modify
prisma/schema.prisma, regenerate with:pnpm build-db-client - Database is already migrated and ready to use
Quick Reference for UI Development
Most Common Imports
// UI Components (from component library)
import { Button, Form, TextField, Modal, Row, Column, Grid } from '@umami/react-zen';
// Custom Components (from common)
import { PageBody, PageHeader, Panel, DataGrid, Empty } from '@/components/common';
// Hooks
import { useApi, useMessages, useFormat, useWebsite, useUser } from '@/components/hooks';
// State
import { useApp } from '@/store/app';
import { setUser, setLocale, setTimezone } from '@/store/app';
Most Common Patterns
- New page: Create in
src/app/(main)/[feature]/page.tsx - New API route: Create in
src/app/api/[feature]/route.ts - New component: Add to
src/components/common/orsrc/components/input/ - New hook: Add to
src/components/hooks/ - New query hook: Add to
src/components/hooks/queries/ - Global state: Add to or use existing
src/store/*.ts
Development Workflow (Niteshift Sandbox)
- Access app - http://localhost:3001 (already running!)
- Login - Use admin/umami credentials
- Make changes - Edit files in
src/ - See results - Hot reload happens automatically, changes appear instantly
- Check types - TypeScript errors show in editor automatically
- Test -
pnpm test(optional) - Lint -
pnpm lint(optional, runs automatically on commit)