Apps
Building Apps
Step-by-step guide to building a Coline app.
Building a Coline App
Create an app that lives inside Coline. This guide walks through building a simple CRM app.
What We're Building
A CRM app that:
- Shows a home view with recent contacts
- Defines a custom "Contact" file type
- Opens contact files with a custom editor
- Indexes contacts for search
Prerequisites
- Node.js 18+
- A Coline workspace
- An API key from the Developer Console
Step 1: Project Setup
Create a Next.js project:
npx create-next-app@latest my-crm --typescript --tailwind --eslint --app
cd my-crm
npm install @colineapp/sdk zodStep 2: Create the Manifest
Create app/manifest.ts:
import { ColineAppManifest } from '@colineapp/sdk'
export const manifest: ColineAppManifest = {
key: 'my-crm',
name: 'My CRM',
description: 'Simple CRM inside Coline',
hosting: {
mode: 'external',
baseUrl: process.env.COLINE_APP_BASE_URL!
},
permissions: [
'files.read',
'files.write',
'index.write',
'notifications.write',
'app.home.read'
],
fileTypes: [{
typeKey: 'contact',
name: 'Contact',
storage: 'coline_document',
indexable: true,
homeRenderMode: 'live'
}],
notificationChannels: [{
key: 'new_contact',
name: 'New Contact',
defaultDeliveries: ['in_app'],
defaultPriority: 'default'
}]
}Step 3: Implement Home View
Create app/api/coline/home/route.ts:
import { NextRequest } from 'next/server'
import { parseSignedColineRequest, colineHostedRenderHomeRequestSchema } from '@colineapp/sdk'
export async function POST(request: NextRequest) {
const { data, deliveryId } = await parseSignedColineRequest({
request,
secret: process.env.COLINE_APP_SECRET!,
schema: colineHostedRenderHomeRequestSchema
})
const { workspace, app } = data
// In production, fetch from your database
const recentContacts = [
{ id: '1', name: 'Alice Smith', company: 'Acme Inc' },
{ id: '2', name: 'Bob Johnson', company: 'Tech Co' }
]
return Response.json({
type: 'stack',
direction: 'vertical',
gap: 6,
padding: 4,
children: [
{
type: 'stack',
direction: 'horizontal',
gap: 2,
children: [
{
type: 'button',
label: '+ New Contact',
action: 'create_contact',
variant: 'primary'
}
]
},
{
type: 'text',
content: `Recent Contacts (${recentContacts.length})`,
style: 'heading'
},
{
type: 'table',
columns: [
{ key: 'name', title: 'Name', width: '40%' },
{ key: 'company', title: 'Company', width: '40%' },
{ key: 'actions', title: '', width: '20%' }
],
rows: recentContacts.map(c => ({
name: c.name,
company: c.company,
actions: {
type: 'button',
label: 'Open',
action: 'open_contact',
payload: { contactId: c.id }
}
}))
}
]
})
}Step 4: Implement File View
Create app/api/coline/file/route.ts:
import { NextRequest } from 'next/server'
import { parseSignedColineRequest, colineHostedRenderFileRequestSchema } from '@colineapp/sdk'
export async function POST(request: NextRequest) {
const { data } = await parseSignedColineRequest({
request,
secret: process.env.COLINE_APP_SECRET!,
schema: colineHostedRenderFileRequestSchema
})
const { file, document } = data
return Response.json({
type: 'stack',
direction: 'vertical',
gap: 4,
padding: 4,
children: [
{
type: 'text_field',
name: 'name',
label: 'Name',
value: document.name || ''
},
{
type: 'text_field',
name: 'email',
label: 'Email',
value: document.email || ''
},
{
type: 'text_field',
name: 'company',
label: 'Company',
value: document.company || ''
},
{
type: 'select',
name: 'status',
label: 'Status',
value: document.status || 'lead',
options: [
{ value: 'lead', label: 'Lead' },
{ value: 'prospect', label: 'Prospect' },
{ value: 'customer', label: 'Customer' }
]
},
{
type: 'stack',
direction: 'horizontal',
gap: 2,
children: [
{
type: 'button',
label: 'Save',
action: 'save_contact',
variant: 'primary'
},
{
type: 'button',
label: 'Delete',
action: 'delete_contact',
variant: 'danger'
}
]
}
]
})
}Step 5: Handle Actions
Create app/api/coline/action/route.ts:
import { NextRequest } from 'next/server'
import { parseSignedColineRequest, colineHostedActionRequestSchema } from '@colineapp/sdk'
import { ColineApiClient } from '@colineapp/sdk'
const client = new ColineApiClient({
baseUrl: 'https://api.coline.app',
apiKey: process.env.COLINE_API_KEY!
})
export async function POST(request: NextRequest) {
const { data } = await parseSignedColineRequest({
request,
secret: process.env.COLINE_APP_SECRET!,
schema: colineHostedActionRequestSchema
})
const { action, workspace, app, file } = data
const ws = client.workspace(workspace.id)
switch (action.type) {
case 'create_contact':
const newFile = await ws.app(app.appKey).createFile({
typeKey: 'contact',
name: 'New Contact'
})
return Response.json({
type: 'open_file',
fileId: newFile.file.id
})
case 'save_contact':
await ws.app(app.appKey).file(file!.id).updateDocument({
name: action.payload.name,
email: action.payload.email,
company: action.payload.company,
status: action.payload.status
})
// Index for search
await ws.app(app.appKey).upsertIndexDocuments([{
documentKey: file!.id,
documentType: 'contact',
fileId: file!.id,
title: action.payload.name,
body: `${action.payload.email} ${action.payload.company}`,
metadata: {
status: action.payload.status
}
}])
return Response.json({ type: 'refresh' })
case 'delete_contact':
await ws.app(app.appKey).file(file!.id).delete()
await ws.app(app.appKey).deleteIndexDocuments([file!.id])
return Response.json({ type: 'navigate', href: '..' })
default:
return Response.json({ type: 'ok' })
}
}Step 6: Environment Variables
Create .env.local:
COLINE_APP_SECRET=your_app_secret_here
COLINE_API_KEY=col_ws_your_api_key
COLINE_APP_BASE_URL=https://your-ngrok-url.ngrok.ioStep 7: Register and Test
Local Development with Ngrok
# Install ngrok if needed
npm install -g ngrok
# Start your app
npm run dev
# In another terminal, expose to internet
ngrok http 3000Copy the ngrok HTTPS URL to COLINE_APP_BASE_URL.
Register the App
Create scripts/register.ts:
import { ColineApiClient } from '@colineapp/sdk'
import { manifest } from '../app/manifest'
const client = new ColineApiClient({
baseUrl: 'https://api.coline.app',
apiKey: process.env.COLINE_API_KEY!
})
async function register() {
const result = await client.registerApp({
version: '1.0.0',
manifest
})
console.log('App registered:', result.appKey)
console.log('Install URL: https://coline.app/developers/apps/' + result.appKey)
}
register()Run it:
npx tsx scripts/register.tsInstall to Workspace
- Go to
https://coline.app/developers/apps/{your-app-key} - Click "Install to Workspace"
- Select your workspace
- Grant permissions
Step 8: Use Your App
- Open your workspace in Coline
- Go to the Apps tab
- Click "My CRM"
- Click "+ New Contact"
- Fill in details and save
Deployment
Production Setup
- Deploy your app to Vercel/Railway/Render
- Update
COLINE_APP_BASE_URLto production URL - Create new app version with updated URL
- Submit for review (for public apps)
Environment Variables
# Production
COLINE_APP_SECRET=production_secret
COLINE_API_KEY=col_ws_production_key
COLINE_APP_BASE_URL=https://my-crm.example.comNext Steps
- Add more file types (Companies, Deals)
- Implement webhooks for real-time sync
- Add Kairo integration for AI features
- Style your UI with Coline design tokens
Troubleshooting
Signature Verification Failed
- Check
COLINE_APP_SECRETmatches the secret shown in Developer Console - Ensure request body isn't being modified
App Not Showing
- Verify app is installed to the correct workspace
- Check browser console for errors
- Verify
baseUrlis accessible from Coline
Actions Not Working
- Check action handler logs
- Ensure response format matches schema
- Verify permissions are granted