feat: redo settings and all org pages. (#601)

* feat: new components, deploy storybook

* refactor: simplify component field definitions and enhance button loading state

- Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook.
- Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props.
- Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback.
- Minor CSS adjustments for better layout and styling consistency.

* chore: add field, card, password components

* feat: improve page component & update AuthUI path

* chore: update bits-ui to version 2.14.4 and all org related UI's

- Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4.
- Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory.
- Removed unused Course components and related files to clean up the codebase.
- Enhanced various components to improve consistency and maintainability.

* chore: move all setting buttons to the top and update auth ui styles

* chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner

- Removed all instances of svelte-loading-spinners from the project.
- Updated components to use the new Spinner from @cio/ui/base/spinner.
- Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners.
- Added new CSS animations for rocket launch effects in app.css.
- Enhanced email change flow with updated verification messages in translations.

* chore: remove supabase file upload from dashboard and move to r2

* chore: update organization API and UI components

- Added new API methods for organization plan operations and team management.
- Introduced middleware for API key authentication and organization admin checks.
- Refactored organization-related components to utilize the new API structure.
- Updated various UI components to improve consistency and maintainability.
- Enhanced error handling and validation for organization-related actions.
- Removed deprecated services and streamlined the codebase.
This commit is contained in:
Rotimi Best
2025-12-02 09:54:38 +01:00
committed by GitHub
parent 531516a8f7
commit c7854efae3
350 changed files with 13928 additions and 3895 deletions
+808 -1
View File
@@ -507,6 +507,22 @@ packages/utils/src/validation/
└── README.md ← Documentation
```
**File Naming Convention**:
All file names should use **lowercase with hyphens** (kebab-case):
- ✅ `setup-progress.svelte` (component files)
- ✅ `org.svelte.ts` (API service files)
- ✅ `course.ts` (validation/query files)
- ✅ `user-profile.svelte` (multi-word component files)
- ❌ `SetupProgress.svelte` (PascalCase - reserved for component names in imports)
- ❌ `Org.svelte.ts` (PascalCase)
- ❌ `userProfile.svelte` (camelCase)
- ❌ `user_profile.svelte` (snake_case)
**Note**: Component names in imports can still use PascalCase (e.g., `import SetupProgress from './setup-progress.svelte'`), but the actual file names must be lowercase with hyphens.
**Example: Course Validations.**
Schemas should be **clean** - only validation rules, no error messages:
@@ -768,14 +784,30 @@ if (!result.success) {
```typescript
import { authMiddleware } from '@api/middlewares/auth';
import { apiKeyMiddleware } from '@api/middlewares/api-key';
import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key';
// Protected route - requires authentication
// Protected route - requires user authentication (session cookies)
.post('/create', authMiddleware, async (c) => {
const user = c.get('user'); // Always available after authMiddleware
const session = c.get('session');
// ...
});
// Protected route - requires API key (server-to-server)
.post('/webhook', apiKeyMiddleware, async (c) => {
// API key validated, proceed
// ...
});
// Protected route - accepts either user session OR API key
.post('/plan', authOrApiKeyMiddleware, async (c) => {
// Can be called by:
// 1. Authenticated users (via session cookies)
// 2. Server-to-server (via API key: Authorization: Bearer <key>)
// ...
});
// Public endpoint - no authentication required
.get('/public-courses', async (c) => {
// Publicly accessible data
@@ -792,6 +824,12 @@ import { authMiddleware } from '@api/middlewares/auth';
});
```
**API Key Authentication:**
- Add `API_KEY` to `apps/api/src/config/env.ts` environment schema
- Use `Authorization: Bearer <api-key>` header format (standard across popular APIs)
- Use `apiKeyMiddleware` for API-key-only endpoints
- Use `authOrApiKeyMiddleware` for endpoints that accept both user sessions and API keys
### Complete Route Example
Routes should be thin - they handle HTTP concerns and delegate to services:
@@ -1004,6 +1042,268 @@ export class ProfileApi {
---
### Example: Creating an Organization (POST Route)
We'll create a `POST /organization` route that creates a new organization with the current user as owner.
---
### Step 1: Create Validation Schema
**Location**: `packages/utils/src/validation/organization/organization.ts`
```typescript
import * as z from 'zod';
export const ZCreateOrganization = z.object({
name: z
.string()
.min(5)
.refine((val) => !/^[-]|[-]$/.test(val), {
message: 'Organization name cannot start or end with a hyphen'
}),
siteName: z
.string()
.min(5)
.refine((val) => !/^[-]|[-]$/.test(val), {
message: 'Site name cannot start or end with a hyphen'
})
});
export type TCreateOrganization = z.infer<typeof ZCreateOrganization>;
```
**Export**: Add to `packages/utils/src/validation/organization/index.ts` and `packages/utils/src/validation/index.ts`
---
### Step 2: Create or Reuse Service Function
**Location**: `apps/api/src/services/organization.ts`
**Option A: Reuse Existing Service** (if function exists in another service)
```typescript
import { createOrganizationWithOwner } from '@api/services/onboarding';
export async function createOrg(profileId: string, data: { name: string; siteName: string }) {
// Reuse existing function from onboarding service
return await createOrganizationWithOwner(profileId, {
orgName: data.name,
siteName: data.siteName
});
}
```
**Option B: Create New Service** (if you need different logic)
```typescript
import { AppError, ErrorCodes } from '@api/utils/errors';
import { db } from '@cio/db/drizzle';
import {
checkSiteNameExists,
createOrganization,
createOrganizationMember,
getOrganizationByProfileId
} from '@cio/db/queries/organization';
import { ROLE } from '@cio/utils/constants';
export async function createOrg(profileId: string, data: { name: string; siteName: string }) {
// Business Logic: Check sitename availability
const exists = await checkSiteNameExists(data.siteName);
if (exists) {
throw new AppError(
`Site name '${data.siteName}' already exists`,
ErrorCodes.SITENAME_EXISTS,
409,
'siteName'
);
}
// Business Logic: Create org and member in a transaction
try {
const result = await db.transaction(async (tx) => {
const organization = await createOrganization({
name: data.name,
siteName: data.siteName
});
const member = await createOrganizationMember({
organizationId: organization.id,
profileId,
roleId: ROLE.ADMIN,
verified: true
});
return { organization, member };
});
// Fetch updated organizations list
const organizations = await getOrganizationByProfileId(profileId);
return {
organization: result.organization,
member: result.member,
organizations
};
} catch (error) {
// Handle database constraint violations
if (error.code === '23505') {
throw new AppError(
`Site name '${data.siteName}' already exists`,
ErrorCodes.SITENAME_EXISTS,
409,
'siteName'
);
}
throw new AppError('Failed to create organization', ErrorCodes.ORG_CREATE_FAILED, 500);
}
}
```
---
### Step 3: Create Route
**Location**: `apps/api/src/routes/organization/organization.ts`
```typescript
import { createOrg } from '@api/services/organization';
import { Hono } from '@api/utils/hono';
import { ZCreateOrganization } from '@cio/utils/validation/organization';
import { authMiddleware } from '@api/middlewares/auth';
import { handleError } from '@api/utils/errors';
import { zValidator } from '@hono/zod-validator';
export const organizationRouter = new Hono()
.post('/', authMiddleware, zValidator('json', ZCreateOrganization), async (c) => {
try {
const user = c.get('user');
const data = c.req.valid('json');
const result = await createOrg(user.id, data);
return c.json({ success: true, data: result }, 201);
} catch (error) {
return handleError(c, error, 'Failed to create organization');
}
});
```
**Note**: Use `201 Created` status code for successful POST requests that create resources.
---
### Step 4: Export and Register Route
```typescript
// apps/api/src/routes/organization/index.ts
export * from './organization';
// apps/api/src/app.ts
import { organizationRouter } from '@api/routes/organization';
export const app = new Hono()
.route('/organization', organizationRouter); // Route available at POST /organization
```
---
### Step 5: Use in Frontend
**Location**: `apps/dashboard/src/lib/features/org/api/org.svelte.ts`
```typescript
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
import { ZCreateOrganization } from '@cio/utils/validation/organization';
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
import { orgs, currentOrg } from '$lib/utils/store/org';
import { snackbar } from '$lib/components/Snackbar/store';
class OrgApi extends BaseApiWithErrors {
async create(fields: { name: string; siteName: string }) {
// Client-side validation
const result = ZCreateOrganization.safeParse(fields);
if (!result.success) {
this.errors = mapZodErrorsToTranslations(result.error, 'organization');
return;
}
await this.execute<typeof classroomio.organization.$post>({
requestFn: () =>
classroomio.organization.$post({
json: result.data
}),
logContext: 'creating organization',
onSuccess: (response) => {
if (!response.data) return;
const { organization, organizations } = response.data;
// Update stores
orgs.set(organizations);
currentOrg.set(organization);
snackbar.success('Organization created successfully');
this.success = true;
this.errors = {};
},
onError: (result) => {
if (typeof result === 'string') {
snackbar.error(result);
return;
}
if ('error' in result && 'field' in result) {
// Field-specific error automatically mapped to this.errors[field]
this.errors[result.field as string] = result.error;
}
}
});
}
}
export const orgApi = new OrgApi();
```
**Key Points:**
- Use `this.execute()` from `BaseApiWithErrors` for automatic loading/error state
- Field-specific errors are automatically mapped to `this.errors[field]`
- Update stores in `onSuccess` callback
- Handle both validation and API errors
---
### Reusing Existing Service Functions
Sometimes a service function exists in a different context (e.g., `createOrganizationWithOwner` in onboarding service).
**When to Reuse:**
- ✅ The function does exactly what you need
- ✅ It's in the same domain (organization operations)
- ✅ You can call it from your new route without modification
**When to Create New:**
- ❌ The existing function has domain-specific logic you don't want (e.g., onboarding-specific)
- ❌ You need different parameters or return values
- ❌ The function is in a different domain and mixing concerns would be inappropriate
**Example: Reusing a Service**
```typescript
// apps/api/src/services/organization.ts
import { createOrganizationWithOwner } from '@api/services/onboarding';
export async function createOrg(profileId: string, data: { name: string; siteName: string }) {
// Reuse existing function, just adapt the parameters
return await createOrganizationWithOwner(profileId, {
orgName: data.name,
siteName: data.siteName
});
}
```
---
### Route Path Convention
When adding routes to an existing router:
@@ -1066,6 +1366,260 @@ When creating a new route, follow this checklist:
---
## Migrating Existing Supabase Calls to API Routes
When you have existing components using direct Supabase calls, follow this migration pattern to move them to the API architecture.
### Migration Workflow
#### Step 1: Identify the Operation
Analyze the existing Supabase call:
- **What database operation?** (CREATE, UPDATE, DELETE, SELECT)
- **What tables are involved?**
- **Are there related operations?** (e.g., create org + create member should be atomic)
- **What validation is currently done?** (client-side only or also server-side?)
**Example Analysis:**
```typescript
// Current code
const { data: org, error } = await supabase
.from('organization')
.insert({ name: orgName, siteName: siteName })
.select();
const { data, error } = await supabase
.from('organizationmember')
.insert({ organization_id: org[0].id, profile_id: $profile.id, role_id: 1 });
// Analysis:
// - Operation: CREATE (organization + member)
// - Tables: organization, organizationmember
// - Related: Yes, should be atomic (transaction)
// - Validation: Client-side only (needs server-side too)
```
#### Step 2: Check for Existing Services
Search for existing service functions that might handle this:
- Check `apps/api/src/services/` for similar operations
- Look in related domains (e.g., onboarding might have org creation)
- Check if the operation exists but in a different context
**Decision:**
- **Reuse existing service** if it does what you need
- **Create new service** if you need different logic or parameters
#### Step 3: Create/Update API Route
If the route doesn't exist:
- Follow the "Creating a New Route" workflow above
- Use appropriate HTTP method: `POST` (create), `PUT` (update), `DELETE` (delete)
If the route exists:
- Add new endpoint to existing router
- Follow route path conventions
#### Step 4: Create Frontend API Method
Add method to existing API class (e.g., `orgApi.create()`):
```typescript
// apps/dashboard/src/lib/features/org/api/org.svelte.ts
class OrgApi extends BaseApiWithErrors {
async create(fields: { name: string; siteName: string }) {
// Client-side validation
const result = ZCreateOrganization.safeParse(fields);
if (!result.success) {
this.errors = mapZodErrorsToTranslations(result.error, 'organization');
return;
}
await this.execute<typeof classroomio.organization.$post>({
requestFn: () =>
classroomio.organization.$post({
json: result.data
}),
logContext: 'creating organization',
onSuccess: (response) => {
// Update stores, navigate, show success
if (response.data) {
orgs.set(response.data.organizations);
currentOrg.set(response.data.organization);
}
snackbar.success('Organization created');
this.success = true;
},
onError: (result) => {
// Error handling (automatic for validation errors)
if (typeof result === 'string') {
snackbar.error(result);
}
}
});
}
}
```
**Key Points:**
- Always use `this.execute()` from `BaseApiWithErrors`
- Client-side validation before API call
- Update stores in `onSuccess` callback
- Errors automatically handled by `BaseApiWithErrors`
#### Step 5: Update Component
Replace Supabase calls with API class method:
**Before (Direct Supabase):**
```typescript
<script lang="ts">
import { supabase } from '$lib/utils/functions/supabase';
import { profile } from '$lib/utils/store/user';
async function createNewOrg() {
// Create organization
const { data: org, error } = await supabase
.from('organization')
.insert({ name: orgName, siteName })
.select();
if (error) {
errors.siteName = error.message;
return;
}
// Create member (separate call - not atomic!)
const { data, error: memberError } = await supabase
.from('organizationmember')
.insert({
organization_id: org[0].id,
profile_id: $profile.id,
role_id: 1
})
.select();
if (memberError) {
// Manual cleanup required
await supabase.from('organization').delete().match({ siteName });
errors.siteName = 'Failed to create member';
return;
}
// Manual store updates
await getOrganizations($profile.id);
goto(`/org/${siteName}`);
}
</script>
```
**After (API Route):**
```typescript
<script lang="ts">
import { orgApi } from '$lib/features/org/api/org.svelte';
import { goto } from '$app/navigation';
async function createNewOrg() {
// Client-side validation (optional, for instant feedback)
errors = createOrgValidation({ orgName, siteName }) as Error;
if (Object.values(errors).length) {
return;
}
// Call API
await orgApi.create({ name: orgName, siteName });
if (orgApi.success) {
// Success - stores already updated by orgApi
goto(`/org/${siteName}`);
$newOrgModal.open = false;
resetForm();
} else {
// Errors automatically mapped to orgApi.errors
if (orgApi.errors.siteName) {
errors.siteName = orgApi.errors.siteName;
} else if (orgApi.errors.general) {
errors.general = orgApi.errors.general;
}
}
}
</script>
```
**Key Differences:**
- ✅ **No manual transaction cleanup** - Handled by service layer
- ✅ **No separate member creation** - Handled atomically by service
- ✅ **Automatic store updates** - Handled by API class `onSuccess`
- ✅ **Consistent error structure** - Field-specific errors automatically mapped
- ✅ **Type-safe** - Full TypeScript support end-to-end
- ✅ **Server-side validation** - Always validated even if client validation passes
### Mapping API Errors to Form Fields
The `BaseApiWithErrors` class automatically maps field-specific errors from the API response:
**API Response Structure:**
```typescript
// Field-specific error
{
success: false,
error: "Sitename already exists",
code: "SITENAME_EXISTS",
field: "siteName" // ← This maps to this.errors.siteName
}
// General error
{
success: false,
error: "Failed to create organization",
code: "ORG_CREATE_FAILED"
// No field property → maps to this.errors.general
}
```
**In Component:**
```typescript
await orgApi.create({ name: orgName, siteName });
if (!orgApi.success) {
// Field-specific error automatically in orgApi.errors.siteName
if (orgApi.errors.siteName) {
errors.siteName = orgApi.errors.siteName;
}
// General error automatically in orgApi.errors.general
if (orgApi.errors.general) {
errors.general = orgApi.errors.general;
}
}
```
**How It Works:**
1. API returns error with `field` property
2. `BaseApiWithErrors.execute()` automatically sets `this.errors[field] = error`
3. Component can access `orgApi.errors.fieldName` directly
4. No manual error mapping needed
### Migration Checklist
When migrating a component from Supabase to API:
- [ ] **Identify operations**: List all Supabase calls in component
- [ ] **Check existing routes**: Search `apps/api/src/routes/` for existing API routes
- [ ] **Check existing services**: Search `apps/api/src/services/` for reusable functions
- [ ] **Create missing pieces**: Follow "Creating a New Route" if route doesn't exist
- [ ] **Create API method**: Add method to appropriate API class using `execute()` pattern
- [ ] **Update component**: Replace Supabase calls with API method
- [ ] **Remove cleanup code**: Delete manual rollback/cleanup (handled by service transactions)
- [ ] **Remove separate calls**: Combine related operations (handled atomically by service)
- [ ] **Update error handling**: Use `orgApi.errors.fieldName` instead of Supabase error structure
- [ ] **Update store sync**: Remove manual store updates (handled by API class `onSuccess`)
- [ ] **Remove Supabase imports**: Delete unused `supabase` imports
- [ ] **Test**: Verify success and error cases work correctly
- [ ] **Test transactions**: Verify atomic operations (e.g., org + member creation)
---
## Step 4: Export and Register Route
### Export Router
@@ -1250,11 +1804,260 @@ Example with **client-side validation** before API call:
</form>
```
### API Abstraction Pattern (API Classes)
When creating API abstractions in `apps/dashboard/src/lib/features/{domain}/api/{entity}.svelte.ts`, **always use the `execute` method** from `BaseApi` or `BaseApiWithErrors` classes. This provides automatic loading state management, error handling, and consistent response processing.
**Important**:
- **`.svelte.ts` files** are for **client-side use only** (Svelte components). They use Svelte reactivity (`$state`, `$derived`) and manage component state. **Never** include API key logic in these files.
- **`.server.ts` files** are for **server-side use only** (`+layout.server.ts`, `+page.server.ts`, `+server.ts`). They contain static methods with API key authentication and are automatically excluded from client bundles by SvelteKit.
#### Using the `execute` Method
**Pattern:**
```typescript
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
class MyApi extends BaseApiWithErrors {
async updateItem(id: string, data: Partial<Item>) {
await this.execute<typeof classroomio.items[':id']['$put']>({
requestFn: () => classroomio.items[':id'].$put({
param: { id },
json: data
}),
logContext: 'updating item',
onSuccess: (response) => {
// Update local stores with response.data
itemsStore.update((items) => {
const index = items.findIndex((i) => i.id === id);
if (index !== -1 && response.data) {
items[index] = { ...items[index], ...response.data };
}
return items;
});
},
onError: (result) => {
// Optional: Custom error handling
if (typeof result === 'string') {
snackbar.error(result);
return;
}
if ('error' in result) {
this.handleValidationError(result);
}
}
});
}
}
export const myApi = new MyApi();
```
**Key Points:**
- **Always use `this.execute`** - Never call the RPC client directly
- **Type the execute call** with `typeof classroomio.{route}.{method}` for type safety
- **Use `requestFn`** - Wrap the RPC call in a function
- **Use `logContext`** - Provide descriptive context for error logging
- **Use `onSuccess`** - Update local stores/reactivity with response data
- **Use `onError`** - Optional custom error handling (validation errors are handled automatically by `BaseApiWithErrors`)
**Examples:**
```typescript
// Profile API - Simple update
await this.execute<typeof classroomio.account.user.$put>({
requestFn: () => classroomio.account.user.$put({ json: profileUpdates }),
logContext: 'updating profile',
onSuccess: (response) => {
profileStore.update((_profile) => ({
..._profile,
...response.profile
}));
}
});
// Onboarding API - With error handling
await this.execute<typeof classroomio.onboarding.step1.$post>({
requestFn: () => classroomio.onboarding.step1.$post({ json: data }),
logContext: 'submitting organization setup',
onSuccess: (result) => {
const { organizations } = result.data;
orgs.set(organizations);
currentOrg.set(organizations[0]);
this.errors = {};
this.step = ONBOARDING_STEPS.USER_METADATA;
},
onError: (result) => {
if (typeof result === 'string') {
snackbar.error(result);
return;
}
if ('error' in result) {
this.handleValidationError(result);
}
}
});
```
**Benefits:**
- ✅ Automatic loading state management (`this.isLoading`)
- ✅ Automatic error state management (`this.error` or `this.errors`)
- ✅ Consistent error handling across all API calls
- ✅ Type-safe responses
- ✅ Centralized logging
- ✅ Easy to test and mock
### Server-Side API Calls
**For server-side files** (`+layout.server.ts`, `+page.server.ts`, `+server.ts`), use **`.server.ts` API classes** instead of calling the RPC client directly. This prevents API keys from being accidentally bundled into client-side code.
#### Pattern for Server-Side Files
Create a `.server.ts` file alongside your `.svelte.ts` API class with static methods that use API key authentication:
```typescript
// apps/dashboard/src/lib/features/org/api/org.server.ts
import { classroomio } from '$lib/utils/services/api';
import { getApiKeyHeaders } from '$lib/utils/services/api/server';
/**
* Server-side API methods for organization operations
* These methods use API key authentication and should only be used in server-side files
* (+server.ts, +layout.server.ts) to prevent API keys from being exposed to the client.
*/
export class OrgApiServer {
/**
* Gets organization by siteName (server-side)
* @param siteName Organization site name
* @returns Organization or null
*/
static async getOrgBySiteName(siteName: string) {
try {
const response = await classroomio.organization.$get(
{
query: { siteName }
},
getApiKeyHeaders()
);
const data = await response.json();
return data.success && data.data && data.data.length > 0 ? data.data[0] : null;
} catch (error) {
console.error('Error fetching organization by siteName (server):', error);
return null;
}
}
}
```
**Usage in server-side files:**
```typescript
// apps/dashboard/src/routes/invite/t/[hash]/+layout.server.ts
import { OrgApiServer } from '$lib/features/org/api/org.server';
export const load = async ({ params }) => {
// Use static method from .server.ts class
const org = await OrgApiServer.getOrgBySiteName(orgSiteName);
return { org };
};
```
#### API Key Authentication for Webhooks/External Calls
For server-to-server communication (webhooks, external integrations), create `.server.ts` files with static methods:
```typescript
// apps/dashboard/src/lib/features/org/api/org-plan.server.ts
import { classroomio } from '$lib/utils/services/api';
import { getApiKeyHeaders } from '$lib/utils/services/api/server';
import type { TCreateOrgPlan } from '@cio/utils/validation/organization';
/**
* Server-side API methods for organization plan operations
* These methods use API key authentication and should only be used in server-side files
* (+server.ts, +layout.server.ts) to prevent API keys from being exposed to the client.
*/
export class OrgPlanApiServer {
/**
* Creates a new organization plan (server-side)
* @param params Organization plan creation parameters
* @returns Response data or null on error
*/
static async createOrgPlan(params: TCreateOrgPlan) {
try {
const response = await classroomio.organization.plan.$post(
{
json: params
},
getApiKeyHeaders()
);
const data = await response.json();
return data.success ? data.data : null;
} catch (error) {
console.error('Error creating org plan (server):', error);
return null;
}
}
}
```
**Usage in webhook handlers:**
```typescript
// apps/dashboard/src/routes/api/polar/webhook/+server.ts
import { OrgPlanApiServer } from '$lib/features/org/api/org-plan.server';
// Use static method from .server.ts class
const result = await OrgPlanApiServer.createOrgPlan(planData);
```
#### File Naming Convention
- **`.svelte.ts`**: Client-side API classes (for Svelte components)
- Example: `org.svelte.ts`, `org-plan.svelte.ts`
- Uses Svelte reactivity (`$state`, `$derived`)
- Manages component state and loading/error states
- **Never** contains API key logic
- **`.server.ts`**: Server-side API classes (for server-side files)
- Example: `org.server.ts`, `org-plan.server.ts`
- Contains static methods with API key authentication
- **Only** used in `+layout.server.ts`, `+page.server.ts`, `+server.ts` files
- Automatically excluded from client bundles by SvelteKit
#### API Key Setup
1. Add `API_KEY` to `apps/api/src/config/env.ts` environment schema
2. Set `API_KEY` environment variable in your deployment
3. Use `authOrApiKeyMiddleware` in API routes that accept both user sessions and API keys
4. Create `.server.ts` files with static methods that use `getApiKeyHeaders()` helper
**Helper Function**: Use `getApiKeyHeaders()` from `$lib/utils/services/api/server` to get API key headers. This helper:
- Returns the correct headers format for RPC client
- Validates that `API_KEY` is configured
- Provides consistent API key authentication across all server-side files
#### Why This Pattern?
- ✅ **Security**: API keys are isolated in `.server.ts` files that cannot be imported on the client
- ✅ **Clear separation**: Client-side (`.svelte.ts`) vs server-side (`.server.ts`) is explicit
- ✅ **Type-safe**: Full TypeScript support end-to-end
- ✅ **Consistent**: Same API routes, same validation, same error handling
- ✅ **Maintainable**: Centralized server-side API methods in dedicated files
- ✅ **SvelteKit convention**: `.server.ts` files are automatically excluded from client bundles
### Frontend Best Practices
#### ✅ DO
- **Use type-safe RPC client** for API calls (Hono RPC)
- **Always use `execute` method** in API abstraction classes (never call RPC client directly)
- **Use `.server.ts` API classes** in server-side files (`+layout.server.ts`, `+page.server.ts`, `+server.ts`)
- **Create `.server.ts` files** with static methods for server-side API calls with API key authentication
- **Use API key** for server-to-server communication (webhooks, external calls)
- **Validate client-side** before API calls using shared schemas from `@cio/utils/validation`
- **Use entity-specific translations** when multiple entities share field names
@@ -1272,6 +2075,10 @@ Example with **client-side validation** before API call:
#### ❌ DON'T
- **Call RPC client directly** in API classes (always use `execute` method)
- **Call RPC client directly in server-side files** - use `.server.ts` API classes instead
- **Use `.svelte.ts` API classes in server-side files** - they're for client-side Svelte components only
- **Include API key logic in `.svelte.ts` files** - API keys must only be in `.server.ts` files
- Skip error handling assuming API calls always succeed
- Display raw API error messages without formatting
- Skip client-side validation (validate before API call for instant feedback)