Intermediate May 3, 2026 · 3 min read

Walk through building a small CRM with three Threads — Customer, Contact, Note — with relationships between them, custom permissions, and a working API. Estimated time: 20 minutes.

What you'll build

  • A Customer Thread (companies you sell to)
  • A Contact Thread (people who work at customers)
  • A Note Thread (free-form notes attached to a customer)
  • Permissions that let Sales users read/write but only Admin delete
  • A working /api/customers, /api/contacts, /api/notes HTTP API

Step 1 — Scaffold

loom new my-crm --module github.com/me/my-crm
cd my-crm

Step 2 — Customer Thread

# threads/customer.yaml
model: Customer
label: Customer
icon: building
description: A company we sell to.

fields:
  - name: company_name
    type: text
    required: true
    searchable: true
  - name: website
    type: url
  - name: industry
    type: select
    default: "other"
    options:
      - { label: SaaS,        value: saas }
      - { label: Manufacturing, value: manufacturing }
      - { label: Other,       value: other }
  - name: notes
    type: markdown

permissions:
  - role: Admin
    can: all
  - role: Sales
    can: [read, create, update]

Step 3 — Contact Thread (linked to Customer)

# threads/contact.yaml
model: Contact
label: Contact
icon: user
description: A person at a customer company.

fields:
  - name: customer_id
    type: link
    target: Customer
    required: true
  - name: full_name
    type: text
    required: true
    searchable: true
  - name: email
    type: email
    required: true
    unique: true
  - name: phone
    type: phone
  - name: title
    type: text

relationships:
  - type: belongs_to
    target: Customer
    foreign_key: customer_id
    label: Customer

permissions:
  - role: Admin
    can: all
  - role: Sales
    can: [read, create, update]

The link field stores the related Customer's UUID; the relationships block describes the cardinality so the generated UI can render a "Customer:" lookup widget.

Step 4 — Note Thread

# threads/note.yaml
model: Note
label: Note
icon: sticky-note
description: A timestamped note attached to a customer.

fields:
  - name: customer_id
    type: link
    target: Customer
    required: true
  - name: subject
    type: text
    required: true
  - name: body
    type: markdown
    required: true

list_view:
  columns: [subject, customer_id, created_at]
  default_sort: created_at
  sort_order: desc
  page_size: 25

permissions:
  - role: Admin
    can: all
  - role: Sales
    can: [read, create, update]

Step 5 — Validate and weave

loom check
loom weave

Each command should report success. loom weave produces 30 files (3 Threads × 10 outputs each).

Step 6 — Bring up the database

export DATABASE_URL='mysql://root:secret@127.0.0.1:3306/my_crm'
loom stitch
# y

loom_migrations audit table created, plus customers, contacts, notes tables.

Step 7 — Serve

export LOOM_SECRET=$(openssl rand -hex 32)
loom serve

You should see startup logs noting the three Threads were mounted under /api/customers, /api/contacts, /api/notes.

Step 8 — Try it

loom serve requires JWT auth. Issue a token from a small Go program — or for quick testing, use --no-auth and grant Public to one of the Threads:

# threads/customer.yaml
permissions:
  - role: Public
    can: [read]    # add temporarily
  - role: Admin
    can: all

Restart loom serve --no-auth and:

curl http://localhost:8080/api/customers
# []
curl -X POST http://localhost:8080/api/customers \
  -H 'Content-Type: application/json' \
  -d '{"company_name":"Acme Inc","website":"https://acme.example.com","industry":"saas"}'
# {"id":"<uuid>","company_name":"Acme Inc",...}

Remove the Public permission before deploying.

What's next

  • Add a before_save hook to lowercase contact emails — see Recipes → Use Hooks to Normalize Data Before Save
  • Add a Stripe webhook to mark Customer subscription_activeTutorials → Adding a Stripe Webhook
  • Deploy this CRM to production — Tutorials → Deploying Loom to Production
Was this article helpful?