Back to Blog
FeaturedTutorials

Building Your First Platform with Spatio Platform SDK

Learn how to build a custom platform using the Spatio Platform SDK. Create a full-stack application that integrates seamlessly with Spatio's workspace.

Matthew Park
January 20, 2024
8 min read

Building Your First Platform with Spatio Platform SDK

The Spatio Platform SDK enables you to build custom platforms that run inside Spatio's workspace. In this tutorial, we'll build a simple task management platform from scratch, learning the core concepts and SDK features along the way.

What You'll Build

By the end of this tutorial, you'll have created a task management platform with:

  • A React frontend that embeds in Spatio
  • Backend API endpoints for task operations
  • Integration with Spatio's workspace and user data
  • Persistent storage using the Platform SDK
  • Real-time collaboration features

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ installed
  • The Spatio Platform CLI installed (npm install -g @spatio/platform-cli)
  • A Spatio account
  • Basic knowledge of React and TypeScript

Step 1: Create Your Platform

First, create a new platform using the CLI:

spatio create task-manager
cd task-manager

When prompted, select the Basic template. This gives us a minimal starting point.

Step 2: Understanding the Project Structure

Your platform has two main parts:

task-manager/
├── src/
│   ├── frontend/          # React application
│   │   ├── App.tsx        # Main component
│   │   └── components/    # UI components
│   └── backend/           # API endpoints
│       └── index.ts       # Backend logic
├── spatio.config.json     # Platform configuration
└── package.json

Step 3: Install Dependencies

Install the Platform SDK and UI components:

npm install @spatio/platform-sdk @spatio/design-system

Step 4: Build the Frontend

Create the Task Interface

First, define our data types. Create src/frontend/types.ts:

export interface Task {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  createdAt: string;
  createdBy: string;
}

Create the Task Component

Create src/frontend/components/TaskItem.tsx:

import { Button, Card } from '@spatio/design-system';
import { Task } from '../types';

interface TaskItemProps {
  task: Task;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}

export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
  return (
    <Card className="p-4 mb-2">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-3 flex-1">
          <input
            type="checkbox"
            checked={task.completed}
            onChange={() => onToggle(task.id)}
            className="h-5 w-5"
          />
          <div className={task.completed ? 'line-through opacity-50' : ''}>
            <h3 className="font-medium">{task.title}</h3>
            <p className="text-sm text-gray-500">{task.description}</p>
          </div>
        </div>
        <Button
          variant="ghost"
          onClick={() => onDelete(task.id)}
        >
          Delete
        </Button>
      </div>
    </Card>
  );
}

Build the Main App

Update src/frontend/App.tsx:

import { useState, useEffect } from 'react';
import { useWorkspace, useSpatioAPI, useStorage } from '@spatio/platform-sdk';
import { Button, Input, Card } from '@spatio/design-system';
import { TaskItem } from './components/TaskItem';
import { Task } from './types';

export default function App() {
  const { workspace, user } = useWorkspace();
  const api = useSpatioAPI();
  const { data: tasks, setData: setTasks } = useStorage<Task[]>('tasks', []);

  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');

  // Load tasks on mount
  useEffect(() => {
    loadTasks();
  }, []);

  const loadTasks = async () => {
    try {
      const fetchedTasks = await api.get<Task[]>('/tasks');
      setTasks(fetchedTasks);
    } catch (error) {
      console.error('Failed to load tasks:', error);
    }
  };

  const createTask = async () => {
    if (!title.trim()) return;

    const newTask: Omit<Task, 'id' | 'createdAt'> = {
      title,
      description,
      completed: false,
      createdBy: user.id,
    };

    try {
      const created = await api.post<Task>('/tasks', newTask);
      setTasks([...tasks, created]);
      setTitle('');
      setDescription('');
    } catch (error) {
      console.error('Failed to create task:', error);
    }
  };

  const toggleTask = async (id: string) => {
    const task = tasks.find(t => t.id === id);
    if (!task) return;

    try {
      const updated = await api.patch<Task>(`/tasks/${id}`, {
        completed: !task.completed,
      });
      setTasks(tasks.map(t => t.id === id ? updated : t));
    } catch (error) {
      console.error('Failed to update task:', error);
    }
  };

  const deleteTask = async (id: string) => {
    try {
      await api.delete(`/tasks/${id}`);
      setTasks(tasks.filter(t => t.id !== id));
    } catch (error) {
      console.error('Failed to delete task:', error);
    }
  };

  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="mb-6">
        <h1 className="text-3xl font-bold mb-2">Task Manager</h1>
        <p className="text-gray-500">
          Workspace: {workspace.name} | User: {user.name}
        </p>
      </div>

      <Card className="p-6 mb-6">
        <h2 className="text-xl font-semibold mb-4">Create New Task</h2>
        <div className="space-y-3">
          <Input
            placeholder="Task title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
          />
          <Input
            placeholder="Description (optional)"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
          />
          <Button onClick={createTask} variant="primary">
            Add Task
          </Button>
        </div>
      </Card>

      <div>
        <h2 className="text-xl font-semibold mb-4">
          Tasks ({tasks.length})
        </h2>
        {tasks.length === 0 ? (
          <p className="text-gray-500 text-center py-8">
            No tasks yet. Create one to get started!
          </p>
        ) : (
          tasks.map(task => (
            <TaskItem
              key={task.id}
              task={task}
              onToggle={toggleTask}
              onDelete={deleteTask}
            />
          ))
        )}
      </div>
    </div>
  );
}

Step 5: Build the Backend

Update src/backend/index.ts:

import { v4 as uuidv4 } from 'uuid';

interface Task {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  createdAt: string;
  createdBy: string;
}

// In-memory storage (replace with database in production)
let tasks: Task[] = [];

export const handlers = {
  // Get all tasks
  'GET /tasks': async (req: any) => {
    return {
      status: 200,
      body: tasks,
    };
  },

  // Create a task
  'POST /tasks': async (req: any) => {
    const { title, description, createdBy } = req.body;

    const task: Task = {
      id: uuidv4(),
      title,
      description: description || '',
      completed: false,
      createdAt: new Date().toISOString(),
      createdBy,
    };

    tasks.push(task);

    return {
      status: 201,
      body: task,
    };
  },

  // Update a task
  'PATCH /tasks/:id': async (req: any) => {
    const { id } = req.params;
    const updates = req.body;

    const taskIndex = tasks.findIndex(t => t.id === id);
    if (taskIndex === -1) {
      return {
        status: 404,
        body: { error: 'Task not found' },
      };
    }

    tasks[taskIndex] = { ...tasks[taskIndex], ...updates };

    return {
      status: 200,
      body: tasks[taskIndex],
    };
  },

  // Delete a task
  'DELETE /tasks/:id': async (req: any) => {
    const { id } = req.params;

    const taskIndex = tasks.findIndex(t => t.id === id);
    if (taskIndex === -1) {
      return {
        status: 404,
        body: { error: 'Task not found' },
      };
    }

    tasks.splice(taskIndex, 1);

    return {
      status: 204,
      body: null,
    };
  },
};

Install the UUID package:

npm install uuid
npm install -D @types/uuid

Step 6: Configure Your Platform

Update spatio.config.json:

{
  "name": "task-manager",
  "displayName": "Task Manager",
  "description": "Simple task management for your workspace",
  "version": "1.0.0",
  "icon": "✓",
  "capabilities": {
    "workspace": true,
    "storage": true
  },
  "routes": {
    "main": "/"
  }
}

Step 7: Test Your Platform

Start the development server:

spatio dev

Open your browser to http://localhost:3000. You should see your task manager running!

Try:

  • Creating new tasks
  • Marking tasks as complete
  • Deleting tasks

Step 8: Deploy Your Platform

When you're ready to deploy:

spatio build
spatio deploy

Your platform is now live and available to your organization!

Next Steps: Advanced Features

Add Real-Time Collaboration

Use the Events API for real-time updates:

import { useEvents } from '@spatio/platform-sdk';

function App() {
  const { emit, on } = useEvents();

  // Emit when a task changes
  const createTask = async () => {
    const task = await api.post('/tasks', newTask);
    emit('task:created', task);
  };

  // Listen for changes from other users
  useEffect(() => {
    const unsubscribe = on('task:created', (task) => {
      setTasks(prev => [...prev, task]);
    });
    return unsubscribe;
  }, []);
}

Connect to External Providers

Access Gmail, Salesforce, and other connected providers:

import { useProviders } from '@spatio/platform-sdk';

function App() {
  const providers = useProviders();
  const gmail = providers.find(p => p.type === 'gmail');

  const createTaskFromEmail = async (emailId: string) => {
    if (!gmail) return;

    const email = await gmail.api.get(`/emails/${emailId}`);
    const task = {
      title: email.subject,
      description: email.body,
    };

    await api.post('/tasks', task);
  };
}

Add Navigation Between Platforms

Navigate to other platforms from your app:

import { useNavigation } from '@spatio/platform-sdk';

function App() {
  const { navigate } = useNavigation();

  return (
    <Button onClick={() => navigate('/calendar')}>
      Open Calendar
    </Button>
  );
}

Key Concepts Recap

Platform SDK Hooks

  • useWorkspace() - Access workspace, user, and organization data
  • useSpatioAPI() - Make authenticated API calls to your backend
  • useStorage() - Persistent storage with automatic sync
  • useProviders() - Access connected third-party services
  • useEvents() - Real-time communication between users
  • useNavigation() - Navigate between platforms

Best Practices

  1. Type Safety: Always use TypeScript for better development experience
  2. Error Handling: Wrap API calls in try-catch blocks
  3. Loading States: Show loading indicators during async operations
  4. Optimistic Updates: Update UI immediately, then sync with backend
  5. Cleanup: Unsubscribe from events in useEffect cleanup

Resources

Conclusion

You've built a fully functional task management platform using the Spatio Platform SDK! This platform:

  • Runs inside Spatio's workspace
  • Has a React frontend and Node.js backend
  • Uses Spatio's authentication and workspace context
  • Stores data persistently
  • Can be extended with real-time features and external integrations

The Platform SDK gives you powerful building blocks to create custom tools tailored to your team's needs. Experiment, iterate, and build amazing platforms!

Happy building! 🚀

Tags

Platform SDKReactDevelopersGetting Started

Related Articles

Enjoyed this article?

Subscribe to our newsletter for more insights and updates