Core Concepts

Server Actions

Server Actions provide type-safe server-side operations for complex queries, aggregations, and business logic that don't fit the DataSource pattern.

When to Use Server Actions

Use CaseSolution
Standard CRUD operationsDataSource + useStore
Paginated lists/tablesDataSource + useStore
Charts with aggregationsActions + useQuery
Reports with custom SQLActions + useQuery
Multi-table joins for analyticsActions + useQuery
Custom business logic mutationsActions + useMutation
WebhooksAPI Routes
File streamingAPI Routes

Action Definition

// src/app/(secure)/module/feature/action.ts
import type { PgPoolClient } from '@wayvo-ai/core/server';
import type { Session } from '@wayvo-ai/core/auth';
import { UserError } from '@wayvo-ai/core/common';

interface SalesDataPoint {
  date: string;
  revenue: number;
  orders: number;
}

export async function getSalesChart(
  client: PgPoolClient,
  session: Session,
  startDate: string,
  endDate: string,
  groupBy: 'day' | 'week' | 'month',
): Promise<SalesDataPoint[]> {
  const result = await client.query<SalesDataPoint>(
    `SELECT 
       DATE_TRUNC($3, order_date) as date,
       SUM(total_amount) as revenue,
       COUNT(*) as orders
     FROM orders
     WHERE order_date BETWEEN $1 AND $2
       AND customer_id = ANY(
         SELECT customer_id FROM user_customers WHERE user_id = $4
       )
     GROUP BY DATE_TRUNC($3, order_date)
     ORDER BY date`,
    [startDate, endDate, groupBy, session.user.id]
  );
  return result.rows;
}

Registering Actions

// src/lib/server/actions/module/index.ts
import { getSalesChart, updateEntityStatus } from './entity-actions';

export const MODULE_ACTIONS = {
  getSalesChart,
  updateEntityStatus,
};

export type ModuleActionName = keyof typeof MODULE_ACTIONS;

export const MODULE_ACTION_ACCESS_ROLES: Record<ModuleActionName, string[]> = {
  getSalesChart: ['all_users'],  // Any authenticated user
  updateEntityStatus: ['admin'],  // Admin only
};

Using Actions with useQuery

import { useQuery } from '@/lib/core/client/useQuery';

function Dashboard() {
  const result = useQuery('getSalesChart', startDate, endDate, 'month');
  
  if (result.status === 'loading') return <Loader />;
  if (result.status === 'error') return <Error message={result.error} />;
  
  const data: SalesDataPoint[] = result.data;
  return <Chart data={data} />;
}

Using Actions with useMutation

import { useMutation } from '@/lib/core/client/useMutation';

function StatusButton({ entityId }: { entityId: string }) {
  const updateStatus = useMutation('updateEntityStatus', {
    invalidateStoresOnSuccess: [{ datasourceId: 'Entity', alias: 'entity-list' }],
  });
  
  const handleClick = async () => {
    await updateStatus.mutate(entityId, 'active');
  };
  
  return (
    <Button onClick={handleClick} disabled={updateStatus.isPending}>
      {updateStatus.isPending ? 'Updating...' : 'Activate'}
    </Button>
  );
}

Action Parameters

Actions follow this pattern:

export async function actionName(
  client: PgPoolClient,    // Database client
  session: Session,        // Current user session
  param1: string,          // Your parameters after session
  param2?: number,         // Optional parameters
): Promise<ReturnType> {
  // Implementation
}

Important: client and session are always the first two parameters. Your custom parameters come after.

Error Handling

import { UserError } from '@wayvo-ai/core/common';

export async function updateEntityStatus(
  client: PgPoolClient,
  session: Session,
  entityId: string,
  status: 'active' | 'inactive',
) {
  const entity = await client.query(
    'SELECT user_id FROM entity WHERE id = $1',
    [entityId]
  );
  
  if (!entity.rows[0]) {
    throw new UserError('Entity not found');  // Shows to user
  }
  
  if (entity.rows[0].user_id !== session.user.id && !session.user.roles.includes('admin')) {
    throw new UserError('Access denied');  // Shows to user
  }
  
  // Throwing other errors logs to server and shows generic error
  const result = await client.query(
    `UPDATE entity SET status = $1 WHERE id = $2 RETURNING *`,
    [status, entityId]
  );
  
  return result.rows[0];
}

Cache Invalidation

const updateStatus = useMutation('updateEntityStatus', {
  // Invalidate specific stores
  invalidateStoresOnSuccess: [
    { datasourceId: 'Entity', alias: 'entity-list' },
  ],
  // Invalidate query actions
  invalidateOnSuccess: ['getEntityStats'],
});

Next Steps

Previous
Stores