Danvas
Danvas
DashboardSupportWelcome

👤 USER DOCS

User Guides

Getting Started

Getting StartedDashboard & OnboardingApp Settings

Tutorials

Tutorial: Setting Up Shift Tasks & ClosersTutorial: Managing Incidents in the InboxTutorial: Tracking Compliance & Sync StatusTutorial: Operational Workflows with the AI AssistantTutorial: Building & Deploying Custom Checklists

Daily Operations (Staff)

Shift Workspace & TasksService Day SetupDaily Line-UpStaff Service Day ReportsForms

Communication & Chat

Messages & AnnouncementsUnified Operations InboxAI Assistant

Manager & Admin Guides

Daily Line-Up SetupStaff SchedulingManaging LocationsNPS and Guest FeedbackCouponsContacts and Guest HistoryManager CloseoutsDaily Line-Up & ComplianceAnalyticsIncident ReportingWhistleblower Concerns & FeedbackAdmin Tools

⚙️ DEVELOPER DOCS

Getting Started

Getting StartedDevelopmentDeployment Guide

Architecture

Architecture OverviewExplanation: AI Integration & Tenant SecurityExplanation: Dynamic Forms Engine DesignExplanation: Compliance Ledger DesignExplanation: Live Sync & Data FreshnessData FlowArchitecture Decision Records

Core Domain

Core DomainDatabase ReferenceLocations DomainAuth & RBACScheduling DomainReports DomainIncidents DomainUnified Operations InboxLive Sync Data FreshnessToast Sync PipelineNotifications DomainCoupons and Guest NPSAudit Log & Compliance ArchitectureDesign Audit FindingsAI Chat IntegrationAnalytics & Tips Integration

Frontend

Frontend ArchitectureFormsLoading SkeletonsComponentsPWA & Offline ShellScreenshots

API Reference

API Reference

Endpoints

POS Sales APIOptimization Data APISchedule Shifts APIEmployee Export APIReports APIIncidents APIAI Chat APIPush Notifications APIWebhooks APICron API

Contributing

ContributingCode Examples

Security

Security & Compliance

Release Notes

What's New

Code Examples

Code patterns used in Danvas development

Danvas uses several key patterns for server actions, database queries, and API integrations.

Server Actions

Danvas uses Next.js server actions for form submissions and data mutations.

Basic Form Submission

// apps/app/app/(authenticated)/forms/actions.ts
'use server'

export async function submitForm(formId: string, data: FormData) {
  const { userId, teamId } = await requireUserAuth();
  const validated = submitFormSchema.parse({ formId, ...Object.fromEntries(data) });

  const submission = await db.insert(formSubmissions).values({
    id: generateId(),
    formId: validated.formId,
    userId,
    teamId,
    data: validated,
    submittedAt: new Date(),
  });

  await logAudit({ teamId, userId, action: 'form.submitted', targetId: submission.id });
  return submission;
}

Database Insert with Audit Log

// Using the audit helper
import { logAudit } from '@repo/database/audit';

await db.insert(incidents).values({
  id: generateId(),
  teamId,
  userId,
  locationId,
  type,
  severity,
  notes,
  createdAt: new Date(),
});

await logAudit({ teamId, userId, action: 'incident.created', targetId: incidentId });

Database Queries

Query with Filters

// Get submissions with optional filters
const submissions = await db.query.formSubmissions.findMany({
  where: and(
    eq(formSubmissions.teamId, teamId),
    optional(formId ? eq(formSubmissions.formId, formId) : undefined),
    optional(locationId ? eq(formSubmissions.locationId, locationId) : undefined),
  ),
  with: {
    form: true,
    location: true,
    user: true,
  },
  orderBy: desc(formSubmissions.submittedAt),
  limit: 50,
});

Upsert Pattern

// Push subscription upsert
await db.insert(pushSubscriptions).values({
  userId,
  endpoint: subscription.endpoint,
  p256dh: subscription.keys.p256dh,
  auth: subscription.keys.auth,
}).onConflictDoUpdate({
  target: [pushSubscriptions.userId, pushSubscriptions.endpoint],
  set: {
    p256dh: subscription.keys.p256dh,
    auth: subscription.keys.auth,
  },
});

Clerk Authentication

Require User Auth

// packages/auth/index.ts
export async function requireUserAuth() {
  const { userId, teamId } = await auth().catch(() => ({ userId: null, teamId: null }));
  if (!userId) throw new Error('Unauthorized');
  return { userId, teamId };
}

Require Admin

export async function requireAdmin() {
  const { userId, teamId } = await requireUserAuth();
  const user = await db.query.users.findFirst({
    where: and(eq(users.clerkId, userId), eq(users.teamId, teamId)),
  });
  if (user?.role !== 'admin') throw new Error('Forbidden');
  return { userId, teamId };
}

Zod Schemas

Form Validation

// apps/app/app/(authenticated)/forms/schemas.ts
import { z } from 'zod';

export const submitFormSchema = z.object({
  formId: z.string().min(1),
  locationId: z.string().min(1),
  data: z.record(z.string(), z.any()),
});

export const incidentSchema = z.object({
  type: z.enum(['Injury', 'Near Miss', 'Spill / Slip Hazard', 'Complaint', 'Maintenance', 'Stockout']),
  severity: z.enum(['low', 'medium', 'high', 'critical']),
  date: z.string(),
  shift: z.enum(['Breakfast', 'Lunch', 'Dinner', 'Late Night', 'Brunch']),
  notes: z.string().max(5000),
  mediaUrls: z.array(z.string()).optional(),
});

Slack Notifications

Build Block Kit Message

// apps/slack-bot/lib/templates/report.ts
export function buildReportBlocks(report: Report, location: Location) {
  return [
    { type: 'header', text: { type: 'plain_text', text: `${location.name} — Shift Report` } },
    { type: 'section', fields: [
      { type: 'mrkdwn', text: `*Sales:* $${report.actualSales} / $${report.goalSales}` },
      { type: 'mrkdwn', text: `*Ratings:* ⭐${report.fohRating} ⭐${report.bohRating}` },
    ]},
    { type: 'context', elements: [{ type: 'mrkdwn', text: `Submitted via Danvas` }] },
  ];
}

Testing

Server Action Test

// __tests__/reports.test.ts
import { submitReport } from '../actions';

test('submitReport creates report and updates compliance', async () => {
  const mockUser = { userId: 'user_123', teamId: 'team_abc' };
  const result = await submitReport({ ...mockReport, userId: mockUser.userId });

  expect(result.id).toBeDefined();
  const compliance = await db.query.compliance.findFirst({
    where: eq(compliance.reportId, result.id),
  });
  expect(compliance.filed).toBe(true);
});

Environment Variables

Required for Development

# apps/app/.env.local
DATABASE_URL=postgresql://user:password@ep-xxx.neon.tech/neondb?sslmode=require
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
AI_GATEWAY_API_KEY=<your-vercel-ai-gateway-key>
CANVAS_TEAM_ID=org_...

Contributing

Guidelines for contributing to the Danvas platform

Security & Compliance

Technical design of authentication, authorization, and data protection

On this page

Server ActionsBasic Form SubmissionDatabase Insert with Audit LogDatabase QueriesQuery with FiltersUpsert PatternClerk AuthenticationRequire User AuthRequire AdminZod SchemasForm ValidationSlack NotificationsBuild Block Kit MessageTestingServer Action TestEnvironment VariablesRequired for Development