A portable personal data stack. Take your data with you.
Haverstack is a structured data store for individuals and small organizations. Apps write Records into your stack — and the stack handles storage, querying, versioning, and permissions, regardless of where data actually lives.
Status: Early development. APIs are unstable.
A stack is a personal or organizational data store. It belongs to one Entity (a person or org) and holds Records — structured data objects that apps create and read.
The key idea: apps only talk to the Haverstack library. They don't know or care whether the underlying storage is a local SQLite database, a folder of JSON files, or a remote server. Switch backends without changing your app.
This is a monorepo. Packages are published to npm under the @haverstack scope.
| Package | Description |
|---|---|
@haverstack/core |
Stack class, types, schema, validation, ID generation |
@haverstack/adapter-sqlite |
SQLite storage adapter |
Planned:
| Package | Description |
|---|---|
@haverstack/adapter-json |
JSON file storage adapter |
@haverstack/adapter-api |
Remote server adapter |
import { Stack } from '@haverstack/core';
import { SQLiteAdapter } from '@haverstack/adapter-sqlite';
// First run — initialize a new stack
const adapter = await SQLiteAdapter.initialize({
path: './my-stack.db',
entityId: 'my-entity-id',
timezone: 'America/New_York',
});
// Subsequent runs — open the existing stack
// const adapter = await SQLiteAdapter.open({ path: './my-stack.db' });
const stack = await Stack.create(adapter);
// Define a type
await stack.defineType('com.example.myapp/note@1', 'Note', {
text: { kind: 'text', required: true },
title: { kind: 'string' },
});
// Create a record
const note = await stack.create('com.example.myapp/note@1', {
text: 'Hello, Haverstack!',
title: 'My first note',
});
// Update it (partial merge — only changed fields needed)
await stack.update(note.id, { title: 'Updated title' });
// Tag it
await stack.associate(note.id, { kind: 'tag', label: 'favourite' });
// Query
const notes = await stack.query({
filter: { typeId: 'com.example.myapp/note@1', tags: ['favourite'] },
sort: { field: 'createdAt', direction: 'desc' },
});The fundamental unit of data. Every record has:
- A Crockford base-32 ID — time-sortable, human-readable, URL-safe
- A type — defined by the app that created it
- Content — a JSON object validated against the type's schema
- Optional:
parentId,entityId,appId,permissions,associations
Types define the schema for a record's content. They are identified by a namespaced, versioned string:
com.example.myapp/note@1
The app author controls the namespace. Two stacks running the same app have the same type IDs and can interop.
Tags, attachments, and relationships are unified under a single model:
{ kind: 'tag', label: 'favourite' }
{ kind: 'attachment', label: 'avatar', fileId: '...', mimeType: 'image/png' }
{ kind: 'relationship', label: 'reply-to', recordId: '...' }Types can evolve over time. Register migration functions between adjacent versions — the library composes them into chains automatically:
await stack.defineType(
'com.example.myapp/note@2',
'Note',
{
text: { kind: 'text', required: true },
title: { kind: 'string', required: false },
},
{ migratesFrom: 'com.example.myapp/note@1' },
);
stack.registerMigration({
from: 'com.example.myapp/note@1',
to: 'com.example.myapp/note@2',
migrate: (content) => ({ ...content, title: '' }),
});Migration is lazy — records are migrated in memory on read, and committed to disk the next time they are updated. Use stack.migrateAll() to commit eagerly.
| Adapter | Use case |
|---|---|
| SQLite | Local app storage, full query support, FTS |
| JSON files | Portable, human-readable, backup/export (planned) |
| Server API | Hosted/shared stacks, permissions enforcement (planned) |
This repo uses pnpm workspaces.
# Install dependencies
pnpm install
# Run all tests
pnpm test
# Typecheck all packages
pnpm typecheck
# Build all packages
pnpm buildpackages/
core/ # @haverstack/core
src/
index.ts # Public exports
types.ts # All type definitions
stack.ts # Stack class
id.ts # Crockford base-32 ID generation
schema.ts # Schema hashing and type compatibility
validate.ts # Content validation
tests/
adapter-sqlite/ # @haverstack/adapter-sqlite
src/
index.ts # SQLiteAdapter
tests/
haverstack/spec— design spec and RFCs (coming soon)haverstack/server— reference server implementation (coming soon)
CC0 1.0 Universal — public domain. No rights reserved.