Building a Multi-Tool Application
Building a Multi-Tool Application
protokit is designed to scale from a single embedded calculator to a suite of dozens of interconnected tools. This page covers the patterns that make large-scale applications manageable.
Architecture overview
A typical multi-tool application has:
┌─────────────────────────────────────────────────────┐
│ Workspace page: /workspace/:id/:tool │
├──────────────────────┬──────────────────────────────┤
│ Sidebar │ Tool area │
│ ─ Tool list │ ─ ProtoTool (or custom UI) │
│ ─ Status indicators │ ─ Connection indicators │
│ ─ Stage groupings │ ─ Status controls │
└──────────────────────┴──────────────────────────────┘
│ │
▼ ▼
Workspace list store Workspace store
─ workspace CRUD ─ ONE shared Y.Doc
─ tool status tracking ─ getToolMap(toolKey)
─ metadata ─ metaMap
One Y.Doc per workspace
The most important architectural decision: all tools within one workspace share a single Y.Doc.
// workspaceStore.ts — factory, one Pinia store per workspace ID
export const useWorkspaceStore = (workspaceId: string) =>
defineStore(`workspace-${workspaceId}`, () => {
const { doc, isReady } = useProtoDoc(`workspace-${workspaceId}`)
// Each tool gets its own Y.Map namespace
function getToolMap(toolKey: string): Y.Map<any> {
return doc.getMap(`fields:${toolKey}`)
}
// Workspace-level metadata
const metaMap = doc.getMap('meta')
function updateMeta(updates: Record<string, any>) {
doc.transact(() => {
Object.entries(updates).forEach(([k, v]) => metaMap.set(k, v))
})
}
return { doc, isReady, getToolMap, updateMeta }
})()
Benefits:
produces/consumeswork instantly and offline — no cross-document wiring needed- One IndexedDB store per workspace instead of one per tool
- One server sync stream per workspace
- Corruption recovery covers the entire workspace at once
Each tool's data lives in its own namespace: fields:estimator, fields:tracker, collection:tasks:items, etc.
The workspace list store
A separate store manages the list of all workspaces — names, icons, tool statuses:
// workspaceListStore.ts — singleton
export const useWorkspaceListStore = defineStore('workspace-list', () => {
const { doc, isReady } = useProtoDoc('workspace-list')
const workspacesMap = doc.getMap('workspaces')
function createWorkspace(input: { name: string; icon?: string }): string {
const id = crypto.randomUUID()
doc.transact(() => {
workspacesMap.set(id, { id, name: input.name, icon: input.icon, createdAt: Date.now() })
})
return id
}
function markToolStatus(workspaceId: string, toolKey: string, status: 'not-started' | 'in-progress' | 'done') {
const ws = workspacesMap.get(workspaceId)
if (!ws) return
const toolsUsed = new Set<string>(ws.toolsUsed ?? [])
const toolsInProgress = new Set<string>(ws.toolsInProgress ?? [])
if (status === 'done') { toolsUsed.add(toolKey); toolsInProgress.delete(toolKey) }
if (status === 'in-progress') { toolsInProgress.add(toolKey); toolsUsed.delete(toolKey) }
if (status === 'not-started') { toolsUsed.delete(toolKey); toolsInProgress.delete(toolKey) }
workspacesMap.set(workspaceId, { ...ws, toolsUsed: [...toolsUsed], toolsInProgress: [...toolsInProgress] })
}
const workspaces = computed(() => [...workspacesMap.values()] as Workspace[])
return { workspaces, isReady, createWorkspace, markToolStatus }
})
This is also a Y.js document — workspace list changes persist offline.
Tool routing
Register pages dynamically in module.ts or nuxt.config.ts:
// In a Nuxt module's setup():
nuxt.hook('pages:extend', (pages) => {
pages.push({
name: 'workspace-tool',
path: '/workspace/:id/:tool',
file: resolve('./runtime/pages/workspace/[id]/[tool].vue'),
})
})
In the route page:
// [tool].vue
const route = useRoute()
const workspaceId = route.params.id as string
const toolKey = route.params.tool as string
const schema = getPrototype(toolKey) // from useProtoRegistry
const workspaceStore = useWorkspaceStore(workspaceId)
// Provide the shared doc to child tools
provide('workspace-doc', workspaceStore.doc)
Scoping tools to a shared doc
When using usePrototype with a shared document, pass the scoped Y.Map directly:
// In SomeToolWrapper.vue
const workspaceDoc = inject('workspace-doc') as Y.Doc
// Instead of creating a new Y.Doc, use the shared one
const fieldsMap = workspaceDoc.getMap(`fields:${toolKey}`)
const { state, derived } = useProtoMap(fieldsMap, schema.fields)
// Collections also scope to the shared doc
const tasks = useProtoCollection(workspaceDoc, `collection:${toolKey}:tasks`, schema.collections.tasks)
Schema registry
Register all your tool schemas at startup:
// plugins/register-schemas.ts
import { estimatorSchema } from '~/schemas/estimator'
import { trackerSchema } from '~/schemas/tracker'
import { dashboardSchema } from '~/schemas/dashboard'
registerPrototype(estimatorSchema)
registerPrototype(trackerSchema)
registerPrototype(dashboardSchema)
Then look up schemas by key anywhere:
const schema = getPrototype(toolKey) // returns PrototypeSchema | undefined
Tool grouping and stages
Organize tools into logical groups (stages, phases, categories) in a config object:
// utils/toolGroups.ts
export const TOOL_GROUPS = [
{
key: 'plan',
label: 'Planning',
icon: 'i-lucide-map',
tools: [
{ key: 'estimator', label: 'Cost Estimator', priority: 'high' },
{ key: 'timeline', label: 'Timeline', priority: 'medium' },
{ key: 'risk-matrix', label: 'Risk Matrix', priority: 'low' },
],
},
{
key: 'track',
label: 'Tracking',
icon: 'i-lucide-activity',
tools: [
{ key: 'task-tracker', label: 'Task Tracker', priority: 'high' },
{ key: 'burn-rate', label: 'Burn Rate', priority: 'medium' },
],
},
]
Use this config to render the sidebar and to validate route params.
Status tracking
const workspaceListStore = useWorkspaceListStore()
// Auto-mark in-progress on first visit
onMounted(() => {
const workspace = workspaceListStore.workspaces.find(w => w.id === workspaceId)
const alreadyStarted = workspace?.toolsUsed?.includes(toolKey) ||
workspace?.toolsInProgress?.includes(toolKey)
if (!alreadyStarted) {
workspaceListStore.markToolStatus(workspaceId, toolKey, 'in-progress')
}
})
// "Mark done" action
function markDone() {
workspaceListStore.markToolStatus(workspaceId, toolKey, 'done')
}
In the sidebar, read these sets to show completion indicators:
toolsUsed.includes(key)→i-lucide-check-circle(done)toolsInProgress.includes(key)→i-lucide-clock(in progress)- neither → default dot
Connection indicators UI
Show the user which tools are feeding data in and which tools need linking:
// useSomeConnections.ts
function useToolConnections(workspaceDoc: Y.Doc, toolKey: string) {
const schema = getPrototype(toolKey)
if (!schema?.consumes) return { active: [], missing: [] }
const active = ref<string[]>([])
const missing = ref<string[]>([])
watchEffect(() => {
const found: string[] = []
const needed: string[] = []
for (const [alias, path] of Object.entries(schema.consumes!)) {
const [sourceKey] = path.split('.')
const outputMap = workspaceDoc.getMap(`outputs:${sourceKey}`)
// Check if source has produced any data
if (outputMap.size > 0) found.push(sourceKey)
else needed.push(sourceKey)
}
active.value = found
missing.value = needed
})
return { active, missing }
}
Render "Data flowing in from Cost Estimator" banners for active connections and "Link Timeline tool to unlock schedule data" prompts for missing ones.
Minimal viable implementation checklist
To build a multi-tool workspace app with protokit:
-
useProtoDoc+ Pinia store for the shared workspaceY.Doc -
getToolMap(toolKey)helper for namespaced Y.Map access - Workspace list store with
createWorkspace,markToolStatus - Schema registry (
registerPrototypein a plugin) - Tool groups config for sidebar rendering
- Dynamic route
/workspace/:id/:tool -
provide('workspace-doc', doc)in the route page -
inject('workspace-doc')in each tool wrapper component - Connection indicator composable for upstream/downstream status
-
ProtoCorruptionModalmounted globally inapp.vue
Overview
Advanced patterns for Nuxt Protokit — custom schemas, building reusable toolkits, and extensibility.
Schema Patterns
Real-world schema patterns — multi-threshold badges, null-safe connections, collection-driven charts, linked question responses, tab layouts with badges, and action toolbars.