Tutorial: build a CRM

This walkthrough builds a small CRM - Companies, Contacts, and Deals - with SvGrid Studio: scaffolded CRUD screens, a related-record picker, a master-detail view, validation, theming, and a real database. Follow it end to end in about fifteen minutes.

By the end you will have three working data screens and understand how the pieces fit together.

Want the no-code version? This exact CRM ships as a ready-made sample app you can open in one click and point at your own data - no coding. See Sample apps (the CRM sample) or Build it visually. This page builds it by hand, in code.

The generated CRUD screen the tutorial produces.

1. Set up

Create a SvelteKit app and install the packages:

npm create svelte@latest crm && cd crm
npm i @svgrid/grid @svgrid/enterprise

Set your license key so the screens ship without a watermark (optional in dev):

// src/routes/+layout.ts
import { setLicenseKey } from '@svgrid/enterprise'
setLicenseKey(import.meta.env.VITE_SVPRO_KEY)

2. Define the data model

Use a Drizzle schema (or point Studio at an existing database - see step 7):

// src/lib/db/schema.ts
import { pgTable, serial, text, integer, timestamp } from 'drizzle-orm/pg-core'

export const companies = pgTable('companies', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  domain: text('domain'),
  tier: text('tier'),
})

export const contacts = pgTable('contacts', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull(),
  companyId: integer('company_id'),
})

export const deals = pgTable('deals', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  amount: integer('amount'),
  stage: text('stage'),
  companyId: integer('company_id'),
  closeDate: timestamp('close_date'),
})

3. Scaffold the screens

Generate a CRUD screen for every table in one command:

npx @svgrid/studio add --all --from src/lib/db/schema.ts
npm run dev

Open /companies, /contacts, /deals. Each is a full screen - sortable, filterable, paginated, with create / edit and multi-select delete. (Without a database the routes start in-memory, so they run immediately; connect a real one in step 7.)

4. Add validation

Open src/lib/contacts.schema.ts and tighten the fields - built-in constraints, no library needed:

{ field: 'name',  type: 'text', required: true, minLength: 2 },
{ field: 'email', type: 'text', required: true, format: 'email' },

Now the contact form rejects an empty name and an invalid email, inline:

Create/edit with built-in validation.

See Edit forms & validation.

5. Relate deals to companies

A deal belongs to a company. Model the relation in src/lib/deals.schema.ts:

{
  field: 'companyId',
  type: 'relation',
  label: 'Company',
  relation: { entity: 'companies', foreignKey: 'companyId', labelField: 'name' },
}

Then give the deal form a company picker by supplying the choices (load companies in the page and map them to options):

const dealForm = {
  ...dealSchema,
  fields: dealSchema.fields.map((f) =>
    f.field === 'companyId'
      ? { ...f, options: companies.map((c) => ({ value: String(c.id), label: c.name })) }
      : f,
  ),
}

Creating a deal now shows a Company dropdown. See Relations.

6. Master-detail: a company and its deals

Give the Companies screen a nested Deals grid. Replace the <SvGrid> in src/routes/companies/+page.svelte with:

<script lang="ts">
  import { SvGridMasterDetail } from '@svgrid/enterprise'
  import { companySchema } from '$lib/companies.schema'
  import { dealSchema } from '$lib/deals.schema'
  // companies + deals loaded from your data source
</script>

<SvGridMasterDetail
  schema={companySchema}
  data={companies}
  detailSchema={dealSchema}
  getChildren={(company) => deals.filter((d) => d.companyId === company.id)}
/>

Click a company row to expand its deals inline. See Master-detail.

7. Connect a real database

Point the same commands at your database - the schema, screens, and forms stay identical:

npm i pg
export DATABASE_URL="postgres://user:pass@localhost:5432/crm"
npx @svgrid/studio add --all --db postgres --url "$DATABASE_URL"

The generated +server.ts routes now read and write your Postgres tables. Works the same for Supabase, MySQL, SQL Server, and SQLite.

8. Theme it

Match your brand by pointing the --sg-* tokens at your design system - the grids, forms, and modals all follow:

:root {
  --sg-accent: hsl(var(--primary));
  --sg-bg: hsl(var(--background));
  --sg-fg: hsl(var(--foreground));
  --sg-radius: var(--radius);
}

See Themes & styling.

9. Ship it

Run the checks and deploy with any SvelteKit adapter:

npx svelte-check
npm run build

See the deployment checklist - license key, DATABASE_URL, connection pooling, and auth on the routes.

Where to go next

See also