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-local |
Local adapter (SQLite + disk) — the common case |
@haverstack/record-adapter-sqljs |
sql.js (SQLite/WASM) StackRecordAdapter |
@haverstack/blob-adapter-disk |
Disk filesystem StackBlobAdapter |
@haverstack/adapter-api |
HTTP adapter for remote stack servers |
Planned:
| Package | Description |
|---|---|
@haverstack/adapter-json |
JSON file storage adapter |
import { Stack } from '@haverstack/core';
import { LocalAdapter } from '@haverstack/adapter-local';
// First run — initialize a new stack
const adapter = await LocalAdapter.initialize({
path: './my-stack.db',
entityId: 'my-entity-id',
timezone: 'America/New_York',
});
// Subsequent runs — open the existing stack
// const adapter = await LocalAdapter.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' },
});
// Tear down when done (flushes pending writes and releases resources)
await stack.flush();
await stack.close();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.
The adapter interface is split into StackRecordAdapter (structured records) and StackBlobAdapter (binary files). Packages follow a naming convention that makes the type clear:
adapter-*— fullStackAdapter(convenience packages that cover both halves)record-adapter-*—StackRecordAdapteronlyblob-adapter-*—StackBlobAdapteronly
| Package | Type | Use case |
|---|---|---|
adapter-local |
full | Local app storage — SQLite records + disk blobs |
record-adapter-sqljs |
record | sql.js records, full query support, FTS |
blob-adapter-disk |
blob | Content-addressed blobs on the local filesystem |
adapter-api |
full | Hosted/shared stacks via HTTP |
adapter-json |
full | Portable JSON files (planned) |
Use combineAdapters({ record, blob }) from @haverstack/core to compose a record adapter with a different blob backend — for example, SQLiteRecordAdapter with a future S3BlobAdapter. adapter-local wraps this pattern for the common case.
This repo uses pnpm workspaces.
# Install dependencies
pnpm install
# Run all tests
pnpm test
# Typecheck all packages
pnpm typecheck
# Build all packages
pnpm builddocs/
spec.md # Design spec — data model, wire format, adapter contract
packages/
core/ # @haverstack/core
src/
index.ts # Public exports
types.ts # All type definitions (StackRecordAdapter, StackBlobAdapter, StackAdapter, …)
stack.ts # Stack class
combine.ts # combineAdapters() — compose record + blob adapters
access.ts # Permission and grant checking
id.ts # Crockford base-32 ID generation
schema.ts # Schema hashing and type compatibility
validate.ts # Content validation
testing.ts # MemoryAdapter test helper (@haverstack/core/testing)
tests/
adapter-local/ # @haverstack/adapter-local
src/
index.ts # LocalAdapter (StackAdapter) — wraps record + blob adapters below
tests/
record-adapter-sqljs/ # @haverstack/record-adapter-sqljs
src/
index.ts # SQLiteRecordAdapter (StackRecordAdapter) + token management
tests/
blob-adapter-disk/ # @haverstack/blob-adapter-disk
src/
index.ts # DiskBlobAdapter (StackBlobAdapter)
tests/
adapter-api/ # @haverstack/adapter-api
src/
index.ts # APIAdapter (StackAdapter)
tests/
The design spec lives in docs/spec.md. It covers the full data model, adapter contract, wire format, and open questions. If you're building an adapter or a server implementation, start there.
haverstack/server— reference server implementation
CC0 1.0 Universal — public domain. No rights reserved.