kinopio-client is the Vue half of a spatial thinking canvas
Kinopio is a creative thinking tool
At a glance
- What is it?
- The kinopio-client repository is the browser application behind Kinopio, a spatial thinking canvas, and it is where the interesting engineering is: a Vue 3 app that keeps spaces, cards and connections in localStorage, queues API operations for later, and receives updates over websockets. It is deployed on Netlify, prerenders its own blog and help pages, and it also builds as a Tauri desktop application.
- Who is it for?
- Adopt kinopio-client if you are building a collaborative canvas, a whiteboard or any spatial interface where the client must work offline, queue changes and apply remote updates, because the local first data model here is the most reusable idea in the repository.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Vue, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A canvas that keeps working when the network does not
Kinopio is a spatial thinking canvas for new ideas and hard problems, and the kinopio-client is the client web app that users read and update spaces, cards and connections with.
The interesting sentence in the README is the one about how that data is saved: it is saved to localStorage and to the kinopio-server via API requests, queued API operations, and websocket broadcasts.
Three mechanisms in one sentence, and each one solves a different problem. The localStorage side means the app has something to show immediately, before the network has said anything, and means it can be opened at all without a connection. The queued API operations are what make the app usable offline, because an action taken while disconnected is not thrown away, it is remembered and sent later. The websocket broadcasts are how a change made by somebody else arrives, without a poll.
The consequences of that design run through the whole codebase, and the README is honest about them in the state table further down. The offline state uses indexedDB and the API queue only. The not signed in state uses indexedDB only. So there are two different local stores, and the application has to know which one it is in before it decides what to do with an edit.
The audience is a frontend developer working on a spatial or collaborative interface, and the second audience is anyone who wants to see how a Vue 3 application handles presence, conflict and reconnection in a way that a canvas demands, because a canvas is where these problems become unavoidable.
The local install wants a real hostname and two global tools
The install section is five commands, and two of them are global installs that are not obvious from the description.
git clone https://github.com/pketh/kinopio-client.git
cd kinopio-client
npm install
npm install -g @vue/cli
npm install -g hostile
hostile set localhost kinopio.localThe last three lines are the interesting part. The Vue CLI is installed globally even though the project is built with Vite, and then the hostile tool is installed globally and used to point a local hostname at the loopback address, so the development server is reached at a name rather than at 127.0.0.1.
The reason for the fake hostname is the HTTPS section further down, which says that to work with code that only works on https, for example clipboard copy and paste, mkcert was used to create a local ssl certificate. A browser treats a page served from a plain IP address differently from a page served from a named host, and a tool like that treats a secure context differently again, so the development setup has to look like a normal site.
Running it is then a single command, with the URL to visit named explicitly:
npm run dev --host
https://kinopio.local:8080The tests and the linter follow the same shape, with npm run test for the test suite and npm run lint for the linter, which the README notes runs on commit.
The one thing to know before you start is that the clone URL in the README is a different organisation from the repository you are reading, which is a small sign of a project that has moved and whose install instructions have not been fully updated.
The store layer is split per item type
The primary files table in the README is the architecture, and it is worth reading as a design rather than a directory listing.
The entry point is main.js, which initialises the router, and the router handles the static pages and the app routes. App.vue is the root component used by all routes. Then the state. stores/useGlobalStore.js is a Pinia store with global interaction state, and stores/useSpaceStore.js is the Pinia store module that handles loading spaces, with each item type in a space having its own store, for example useCardStore.js and useBoxStore.js.
So the state is not one big store, it is one store per kind of object plus a global one for interaction state. That is the same shape as the components, since components/Card.vue displays cards from the card store, components/Connection.vue displays connections from the connection store and components/Box.vue displays boxes from the box store.
The two files that explain the design are utils.js and views/Space.vue. utils.js holds the functional methods that do dom manipulations or common tasks, and the README is explicit that these cannot access components or the store directly, which is the rule that keeps the DOM layer free of state. views/Space.vue contains the core interaction layer, sending user inputs to the painting, connecting and dragging components, and it is where new connections are created and checked to see whether they connect.
The layers directory holds the canvas work, with components/layers/PaintSelectCanvas.vue handling the paint select strokes for multiple card and connection selection, revealing the multiple selected actions, locking scroll on touch, and providing the other canvas elements that need to cover the viewport, and components/layers/DrawingStrokes.vue holding the space drawing strokes.
That is a canvas application in which the rendering, the state and the input handling are three separate layers, which is the arrangement that makes the rest of the app possible.
Eight user states, listed because they are the hard part
The README has a table of user states to design for, and its presence tells you what the team considers difficult about this application.
| `offline` | indexedDB and API queue operations only |
| `not signed in` | indexedDB only |
| `space is read only` | cannot add cards or edit |
| `space is open` | can add cards, can only edit cards they created |
| `mobile` | touch handlers, no hover, small screen |
| `desktop zoom out` | using the zoom bar or cmd+/- |
| `pinch zoom out/in` | using native touch gesture on mobile |
| `group member or admin` | can see and edit all spaces in the group |The first four are the state machine of a collaborative document, and they interact. Offline means the queue is the only way to change anything. Not signed in means there is no server at all, so only the local store. A read only space means edits are rejected. An open space means you can add cards but only edit the ones you created, which is a per object permission rather than a per space one.
The last four are about the canvas itself, and they are the reason a spatial interface needs its own design vocabulary. Hover does not exist on touch, so every hover interaction needs a touch equivalent, and a pinch gesture has to coexist with a canvas that is already pannable. Zooming out with the zoom bar or with cmd and minus is a desktop concern, and the layout has to survive it.
Reading this table tells you more about the project than the feature list would. A team that writes down its states this way is designing for the states rather than discovering them, and a reader looking for the seams in the implementation should look at how these eight cases are handled in the stores and in Space.vue.
The site is prerendered, and the blog and help are markdown
Kinopio is not only an application, it is a website with an application inside it, and the README documents the publishing pipeline in unusual detail.
During the deploy and build process, which is the npm run build command, vite-ssg generates static HTML pages of the routes defined in vite.config.js in ssgOptions.includedRoutes. Static pages, compiled from the vue router into /dist, are served to the client directly, and the client only goes through the vue router for non-static routes like /app. For unfurling, static pages are specified in page-meta.js, and to test the pre-rendered page routes you use npm run build-dev.
The reason is search engines. The Netlify prerender extension compiles space URLs into static html so that they are scannable by search engines which cannot run client js, which matters here because the spaces are the content of the site.
The page-meta.js file is also an edge function. The README describes /edge-functions/page-meta.js as an edge function that runs in an isolated server-side container before page requests, and that it writes index.html metatags for title, description and similar for crawlers. And there is an honest note about testing it: the author could not figure out how to run the netlify-cli locally, so the testing is done in staging using PR deploy URLs, with the logs under Netlify project, then Deploys, then Choose PR deployment, then Edge Functions.
The content itself is markdown. Blog posts are markdown files in src/blog, and adding one publishes it, so src/blog/my-post.md becomes /blog/my-post and is prerendered to its own static html file so that robots and link unfurlers see the full post. Post media lives on the kinopio-updates linode s3 bucket, referenced with cdn urls. The help posts are statically compiled markdown files in src/help, in the same way.
A Tauri desktop build, notarised and signed
The last section of the README, and the package scripts, reveal that this is not only a web app.
The package.json scripts include a desktop pair, where the desktop command runs tauri dev with a dev runner script, and the desktop build command invokes the Tauri CLI to build for the universal apple darwin target, using an env file if it exists. There are hooks around it too, a predesktop build that runs a compile icon script from the src-tauri directory, and a postdesktop build that notarizes the dmg and then runs an update files script.
So the desktop build is a signed and notarised macOS application with a universal binary, built from the same Vue code as the web app. The repository has a src-tauri/ directory for it, and a build/ directory at the top level for the desktop build output.
The rest of the scripts are the web build. The dev script runs Vite with the host flag, the build script runs vite-ssg build, there is a development build variant that sets the MODE environment variable to development through cross-env, and a preview script. The test script runs vitest, the lint script runs eslint over the src directory, and the build-dev script runs nodemon, ignoring the dist directory, and reruns the development build and the preview on change.
The dependency list explains the stack. Vue 3.5 and vue-router 4 are the framework, Pinia is the state layer, and the rest are the canvas and content tools: idb-keyval for the indexedDB side of the local store, adaptive-quadratic-curve and overlap-area for geometry, colord for colour, temml for the maths rendering used by the card content, macrolight for the lighting effect, fractional-indexing for ordering, dayjs for dates, and lodash-es as the utility library. The devDependencies add vitest, the Vue test utils, Tauri CLI 2.12, and the ESLint and Prettier tooling.
The licence field on the repository records a custom licence that GitHub cannot classify, and the root holds a LICENSE.md file, so the terms to read are in that file rather than in a standard identifier. The last push to the repository was on 2026-10-20, and the Netlify badge at the top of the README points at the deploy status of the production site.
Editorial conclusion
Adopt kinopio-client if you are building a collaborative canvas, a whiteboard or any spatial interface where the client must work offline, queue changes and apply remote updates, because the local first data model here is the most reusable idea in the repository. Do not adopt it if you need a general purpose application shell, because most of the code is canvas specific, and note the licence, which the repository records as a custom licence that GitHub cannot classify, so read LICENSE.md before you copy code from it. Before you start, read the two linked documents on the architecture and on how the data is saved, then work through the eight user states listed in the README, because the offline, not signed in and read only cases are where the design decisions are visible.
Frequently asked questions
What is kinopio-client?
It is the client web app for Kinopio, a spatial thinking canvas. Users read and update spaces, cards and connections, and the data is saved to localStorage and to the kinopio-server through API requests, queued API operations and websocket broadcasts.
How do I run the kinopio-client locally?
Clone the repository, run npm install, install the Vue CLI and the hostile tool globally, and use hostile to map a local hostname to the loopback address. Then run npm run dev --host and open the kinopio.local URL on port 8080.
How does kinopio-client handle being offline?
The README lists the user states to design for, and the offline state uses indexedDB and API queue operations only, while the not signed in state uses indexedDB only. A separate table covers the read only space, the open space, mobile, zoom and group member states.
How are the Kinopio blog and help pages published?
Blog posts are markdown files in src/blog, and adding one turns it into a /blog route that is prerendered to its own static HTML file so crawlers see the full post. Help posts are statically compiled markdown files in src/help, and post media is referenced from the kinopio-updates s3 bucket with CDN URLs.
Does kinopio-client build as a desktop application?
Yes. The package scripts include a Tauri dev command and a build command targeting universal apple darwin, with a pre step that compiles the icon and a post step that notarizes the dmg and updates files. The web build itself uses vite-ssg build, and the tests run under vitest.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/kinopio-club-kinopio-client)Was this useful?