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 Case | Solution |
|---|---|
| Standard CRUD operations | DataSource + useStore |
| Paginated lists/tables | DataSource + useStore |
| Charts with aggregations | Actions + useQuery |
| Reports with custom SQL | Actions + useQuery |
| Multi-table joins for analytics | Actions + useQuery |
| Custom business logic mutations | Actions + useMutation |
| Webhooks | API Routes |
| File streaming | API 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
- Data Fetching Decision Tree - When to use what
- Stores - For standard CRUD operations