Rules for building full-stack features with frontend and backend changes.
Follow these rules when creating features that span the admin-portal frontend and Node.js backend.
Add an exported function in /admin-portal/src/jupiter-api/index.ts using the authenticated request wrappers:
// Example: GET request
export const getInvoices = (clientId: string) =>
authenticatedGet<Invoice[]>(`/admin/client/${clientId}/invoices`);
// Example: POST request
export const createPayment = (data: CreatePaymentRequest) =>
authenticatedPost<Payment>('/admin/payment', data);
// Example: PATCH request
export const updateClient = (clientId: string, payload: any) =>
authenticatedPatch<ClientInfo>(`/client/${clientId}`, payload);
// Example: DELETE request
export const deletePaymentRequest = (id: string) =>
authenticatedDelete<void>(`/admin/payment_request/${id}`, {});
// Example: Public/unauthenticated POST request
export const login = (email: string, password: string) =>
post<{ token: string }>('/login', { email, password });
Available request wrappers:
Authenticated (includes auth token from localStorage):
authenticatedGet<T>(uri: string) - GET with auth tokenauthenticatedPost<T>(uri: string, body: any) - POST with auth tokenauthenticatedPatch<T>(uri: string, body: any) - PATCH with auth tokenauthenticatedDelete<T>(uri: string, body: any) - DELETE with auth tokenfetchWithAuth<T>(path: string, options?: RequestInit) - Deprecated. Prefer using other wrappers. Custom fetch with auth tokenUnauthenticated (for public endpoints):
post<T>(uri: string, body: PostBody) - POST without auth tokenget<T>(uri: string) - GET without auth token (not exported, use authenticatedGet instead)// For GET requests
const { data, isLoading } = useQuery({
queryKey: ['invoices', clientId],
queryFn: () => getInvoices(clientId),
});
// For mutations (POST/PATCH/DELETE)
const mutation = useMutation({
mutationFn: createPayment,
onSuccess: () => queryClient.invalidateQueries(['invoices']),
});
Refer to @../admin-portal-frontend-components/SKILL.md
Admin routes typically follow these patterns:
/backend/app.js under router.use('/admin', Auth.rejectNonSnoutUsers) (line 516+)/backend/app.js main router with Auth.hasVetId() or Auth.rejectNonSnoutUsers/backend/ in semantically named files (e.g., clients.js, payment_request.js, vets.js)Common auth middleware:
Auth.rejectNonSnoutUsers - Only allow Snout internal users (admins)Auth.hasVetId() - Require vet_id in token (vets or admins viewing vet data)// Admin-only route example
router.get('/admin/payment_requests',
Auth.rejectNonSnoutUsers,
PaymentRequest.listPaymentRequests
);
// Vet or admin route example
router.get('/invoices',
Auth.hasVetId(),
Invoices.listInvoicesAdmin
);
backend/clients.js, backend/payment_request.js, backend/vets.js/backend/services/ - business logic/backend/middleware/ - auth, validation, etc./backend/workflows/ - complex multi-step operations/backend/dml/ - data access layerE2E tests use Playwright and live in /admin-portal/e2e/.