Server Actions (Remote Functions)
Overview
Section titled “Overview”ElyOS uses SvelteKit’s command and query functions for server-side logic. These live in *.remote.ts files and can be called directly from the client — no need for separate API routes.
src/apps/[app-name]/└── [feature].remote.ts # Server actionscommand vs query
Section titled “command vs query”| Type | When to Use | Mutation? |
|---|---|---|
command | Data modification, save, delete | Yes |
query | Data reading, listing | No |
Basic Structure
Section titled “Basic Structure”import { command, query, getRequestEvent } from '$app/server';import * as v from 'valibot';
// Validation schemaconst updateSettingsSchema = v.object({ theme: v.optional(v.object({ mode: v.optional(v.picklist(['light', 'dark', 'auto'])) }))});
// Mutation – commandexport const updateSettings = command(updateSettingsSchema, async (input) => { const { locals } = getRequestEvent();
if (!locals.user?.id) { return { success: false, error: 'Not logged in' }; }
// ... database operation
return { success: true };});
// Reading – queryexport const getSettings = query(async () => { const { locals } = getRequestEvent(); // ... fetch data return { success: true, data: locals.settings };});Return Value Convention
Section titled “Return Value Convention”Every remote function returns an object of shape { success: boolean, error?: string, ...data }:
// Successful responsereturn { success: true, data: result };
// Error responsereturn { success: false, error: 'Descriptive error message' };Validation
Section titled “Validation”The first parameter of command is always a Valibot schema. Input is automatically validated — if validation fails, the handler doesn’t run.
import * as v from 'valibot';
const createUserSchema = v.object({ name: v.pipe(v.string(), v.minLength(2), v.maxLength(100)), email: v.pipe(v.string(), v.email()), role: v.picklist(['admin', 'user']), age: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(150)))});
export const createUser = command(createUserSchema, async (input) => { // input type is automatically v.InferOutput<typeof createUserSchema> console.log(input.name, input.email); // ...});Detailed Valibot documentation — including client-side usage, type inference, and custom error messages — can be found on the Validation page.
Session and Permission Checking
Section titled “Session and Permission Checking”getRequestEvent() returns the current request context, including locals:
import { command, getRequestEvent } from '$app/server';
export const deleteItem = command(deleteSchema, async (input) => { const { locals } = getRequestEvent();
// Check authentication if (!locals.user?.id) { return { success: false, error: 'Authentication required' }; }
// Check admin permission // (permission management is done via permissionStore) const userId = parseInt(locals.user.id);
// ...});locals Contents
Section titled “locals Contents”The App.Locals interface defined in app.d.ts:
interface Locals { user: import('better-auth').User | null; session: import('better-auth').Session | null; settings: UserSettings; locale: string;}Client-Side Call
Section titled “Client-Side Call”Remote functions can be imported and called directly from Svelte components:
<script lang="ts"> import { updateSettings } from './settings.remote'; import { toast } from 'svelte-sonner';
async function save() { const result = await updateSettings({ theme: { mode: 'dark' } });
if (result.success) { toast.success('Settings saved'); } else { toast.error(result.error ?? 'An error occurred'); } }</script>
<button onclick={save}>Save</button>Timeout Handling
Section titled “Timeout Handling”If the server doesn’t respond, the withTimeout helper prevents UI freezing:
import { withTimeout, RemoteTimeoutError } from '$lib/utils/remote';
try { const result = await withTimeout(fetchData({}), 8000);} catch (error) { if (error instanceof RemoteTimeoutError) { toast.error('Server did not respond in time'); }}Paginated Queries
Section titled “Paginated Queries”Convention for returning paginated data:
const fetchItemsSchema = v.object({ page: v.optional(v.pipe(v.number(), v.minValue(1)), 1), pageSize: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(100)), 20), search: v.optional(v.string())});
export const fetchItems = command(fetchItemsSchema, async (input) => { const limit = input.pageSize ?? 20; const offset = ((input.page ?? 1) - 1) * limit;
const [rows, totalCount] = await Promise.all([ itemRepository.findMany({ limit, offset, search: input.search }), itemRepository.count({ search: input.search }) ]);
return { success: true, data: rows, pagination: { page: input.page ?? 1, pageSize: limit, totalCount, totalPages: Math.ceil(totalCount / limit) } };});Type Exporting
Section titled “Type Exporting”It’s useful to export the schema output type for client-side use:
export type CreateUserInput = v.InferOutput<typeof createUserSchema>;