Skip to content

Repository files navigation

SellLocal by Cephable

SellLocal by Cephable

A local-first CRM for managing prospect outreach — built as an open source example of how to integrate the Cephable Web SDK for voice navigation and control inside a desktop app.

Want to add voice commands to your own app? Head to developers.cephable.com to create a project and get your SDK credentials.


What this app demonstrates

  • Registering custom voice commands that map to app-specific actions (navigate to a page, search contacts, create records)
  • Defining named entities so the SDK can extract structured values from free-form speech ("go to contacts", "create campaign called Q3 Outreach")
  • Supporting both guest mode (no sign-in required) and linked Cephable account (synced profiles and advanced controls)
  • Handling the OAuth deep-link callback inside Electron so the browser-based sign-in flow completes cleanly
  • Swapping the active microphone at runtime without restarting the voice service

Tech stack

Layer Technology
Desktop shell Electron via electron-vite
UI React 18, MUI v5, React Router v6
Data layer SQLite via better-sqlite3, mirrored to CSV
State Zustand, TanStack Query
Voice / accessibility @cephable/cephable-web
Language TypeScript throughout

Getting started

Prerequisites

1. Clone and install

git clone https://github.com/cephable/local-crm.git
cd local-crm
npm install

2. Configure environment variables

Copy the example env file and fill in your Cephable credentials:

cp .env.example .env

Open .env and set:

VITE_CEPHABLE_CLIENT_ID=your_client_id
VITE_CEPHABLE_CLIENT_SECRET=your_client_secret   # optional for public clients
VITE_CEPHABLE_DEVICE_TYPE_ID=your_device_type_id # optional

Get these values from your project dashboard at developers.cephable.com.

3. Run in development mode

npm run dev

This starts the Electron app with hot reload.

4. Build a distributable

npm run package:win    # Windows NSIS installer
npm run package:mac    # macOS DMG + ZIP
npm run package:linux  # AppImage + .deb

Project structure

src/
  main/           # Electron main process
    ipc/          # IPC handlers (contacts, campaigns, templates, etc.)
    db/           # SQLite connection + schema
  preload/        # Context bridge — exposes typed API to renderer
  renderer/
    components/
      voice/      # VoiceAssistant.tsx — the floating mic panel UI
    context/
      CephableVoiceContext.tsx  # SDK initialization + state management
    pages/        # Today queue, Contacts, Campaigns, Templates, Import, Settings
    hooks/        # useKeyboardShortcut, useKeySequence
  shared/
    types.ts      # Shared TypeScript types
    ipcChannels.ts

How the Cephable SDK integration works

1. Install the SDK

npm install @cephable/cephable-web

2. Define your custom controls

Custom controls tell the SDK what commands your app understands and which spoken phrases trigger them. Each control can include named entities (variables extracted from speech).

// src/renderer/context/CephableVoiceContext.tsx
const MY_CUSTOM_CONTROLS = [
  {
    id: 'nav_page',
    name: 'Navigate',
    description: 'Go to a page in the app',
    defaultCommands: ['go to @page', 'navigate to @page', 'open @page'],
  },
  {
    id: 'search_contacts',
    name: 'Search Contacts',
    defaultCommands: ['search contacts for @query', 'find contact @query'],
  },
  // ...more controls
];

3. Define named entities

Named entities let the SDK extract structured values from a spoken phrase. For example, @page in "go to contacts" resolves to the string "contacts".

const MY_ENTITIES = {
  page: {
    options: {
      contacts: ['contacts', 'people', 'leads'],
      campaigns: ['campaigns', 'outreach'],
      settings: ['settings', 'preferences'],
    },
  },
  query: {
    trim: [{ type: 'position', position: 'after', words: ['for'] }],
  },
};

4. Initialize the service

import { CephableService } from '@cephable/cephable-web';

const service = new CephableService({
  authenticationConfiguration: {
    clientId: CLIENT_ID,
    clientSecret: CLIENT_SECRET,    // optional
    redirectUri: 'myapp://auth/callback',
    autoRefresh: true,
  },
  deviceName: 'My App',
  locale: 'en-US',
  includeDefaultControls: true,       // built-in scroll, click, focus commands
  enableIntelligentCommands: true,
  customControls: MY_CUSTOM_CONTROLS,
  customEntities: MY_ENTITIES,
  onCustomControlAction: (control, _command, _input, intent, entities) => {
    // dispatch to your app logic here
    handleVoiceAction(control.id, entities);
    return true;
  },
});

// Guest mode — no sign-in required
const voiceConfig = {
  locale: 'en-US',
  onPartialResult: (text) => setTranscript(text),
  onFinalResult: (text, result) => setLastCommand(text, result),
  onListeningStarted: () => setStatus('listening'),
  onListeningStopped: () => setStatus('idle'),
};

await service.initializeWithGuestUser(voiceConfig, null);
await service.voiceService.startVoiceControls();

5. Handle the action callback

The onCustomControlAction callback fires when the SDK recognizes a command. Entities contain the extracted values:

function handleVoiceAction(controlId: string, entities: DetectedEntity[]) {
  const getEntity = (name: string) => entities?.find(e => e.entity === name);

  switch (controlId) {
    case 'nav_page': {
      const page = getEntity('page')?.option;  // e.g. "contacts"
      navigate(routes[page]);
      break;
    }
    case 'search_contacts': {
      const query = getEntity('query')?.utteranceText;
      navigate(`/contacts?search=${encodeURIComponent(query)}`);
      break;
    }
  }
}

OAuth (linked account) flow in Electron

When a user links their Cephable account, the SDK opens the browser for sign-in. On completion, Cephable redirects to a custom protocol URL (selllocal://auth/callback). Electron catches this deep link and passes it back to the renderer:

// main process — src/main/index.ts
app.setAsDefaultProtocolClient('selllocal');
app.on('open-url', (event, url) => handleDeepLink(url)); // macOS
app.on('second-instance', (_e, argv) => {                 // Windows/Linux
  const url = argv.find(a => a.startsWith('selllocal://'));
  if (url) handleDeepLink(url);
});

function handleDeepLink(url: string) {
  mainWindow.webContents.send('cephable:deep-link', url);
}

// renderer — src/renderer/context/CephableVoiceContext.tsx
api.cephable.onDeepLink(async (url) => {
  const { code, state } = parseCallback(url);
  await service.authenticationService.authenticateFromCode(code, state);
  await service.initializeWithExistingUser(voiceConfig, null);
  await service.voiceService.startVoiceControls();
});

Voice commands

Category Example phrases
Navigate "Go to today", "Open contacts", "Navigate to campaigns", "Show settings"
Contacts "Search contacts for [name]", "Find [company]", "Create contact"
Campaigns "Create campaign", "New campaign called [name]"
Today queue "Send today's emails", "Open today queue", "Log a call"
Templates "Create template", "New email template"
App controls "Click [element]", "Scroll down", "Scroll to top", "Focus next"

App controls (scroll, click, focus) come from the SDK's built-in controls via includeDefaultControls: true.


CRM features

  • Today queue — daily digest of emails due for each enrolled contact, opens a mailto: link pre-filled with the rendered template
  • Contacts — manage prospects with status tracking (New → In Progress → Qualified / Lost) and BANT qualification fields
  • Campaigns — multi-step drip sequences with configurable day spacing between steps
  • Templates — email templates with {{firstName}}, {{company}}, and custom field merge tags
  • CSV import — drag-and-drop import with field mapping, auto-saved to the workspace folder
  • Local-first — all data lives in a SQLite file inside a user-chosen workspace folder; contacts are mirrored to CSV for easy export

Adapting this for your own app

  1. Create a project at developers.cephable.com and copy your credentials into .env.
  2. Replace SELLLOCAL_CUSTOM_CONTROLS in CephableVoiceContext.tsx with commands relevant to your app.
  3. Replace SELLLOCAL_ENTITIES with the named entity options your commands need.
  4. Update handleAction in VoiceAssistant.tsx to map controlId values to your app's navigation or actions.
  5. If you're not using Electron, remove the deep-link handling in main/index.ts and replace api.cephable.onDeepLink with whatever your framework provides for custom URL schemes.

Contributing

Issues and pull requests are welcome. Please open an issue before submitting large changes so we can discuss the approach.


License

MIT — see LICENSE.

About

A local running CRM for managing communications with prospects using locally synced files for easy export and shifting

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages