Skip to content

Schemas

BitMarks uses Zod for runtime validation. All schemas are available in the @bitmarks.sh/core package.

The main bookmark schema (decrypted form):

import { z } from 'zod';
export const BookmarkSchema = z.object({
id: z.string(),
title: z.string(),
url: z.string().url().optional(),
parentId: z.string().optional(),
dateAdded: z.string().datetime().optional(),
dateModified: z.string().datetime().optional(),
folder: z.string().optional(),
path: z.string().optional(),
tags: z.array(z.string()).optional(),
favicon: z.string().optional(),
domain: z.string().optional(),
source: SourceTypeSchema,
// GitHub star fields
name: z.string().optional(),
full_name: z.string().optional(),
stargazers_count: z.number().optional(),
description: z.string().nullable().optional(),
owner: GitHubOwnerSchema.optional(),
});
export type Bookmark = z.infer<typeof BookmarkSchema>;
export const SourceTypeSchema = z.enum([
'bookmarks', // Browser bookmarks
'history', // Browsing history
'groups', // Tab groups
'stars' // GitHub stars
]);
export type SourceType = z.infer<typeof SourceTypeSchema>;

The encrypted envelope sent to/from the API:

export const EncryptedDataSchema = z.object({
encrypted_data: z.string(), // Base64-encoded ciphertext
data_hash: z.string() // SHA-256 of plaintext
});
export type EncryptedData = z.infer<typeof EncryptedDataSchema>;

Events in the sync log:

export const SyncEventSchema = z.object({
id: z.string(),
user_id: z.string(),
entity_type: z.enum(['bookmark', 'history', 'group']),
entity_id: z.string(),
operation: z.enum(['create', 'update', 'delete']),
timestamp: z.number().int().positive(),
device_id: z.string(),
encrypted_data: z.string().optional(),
data_hash: z.string(),
conflict_resolved: z.boolean(),
resolution_strategy: z.string().optional()
});
export type SyncEvent = z.infer<typeof SyncEventSchema>;

User sync settings:

export const SyncConfigSchema = z.object({
enabled: z.boolean().default(true),
autoSync: z.boolean().default(true),
syncInterval: z.number().default(300000), // 5 minutes
conflictStrategy: z.enum(['last-write-wins', 'manual']).default('last-write-wins'),
sources: z.array(SourceTypeSchema).default(['bookmarks'])
});
export type SyncConfig = z.infer<typeof SyncConfigSchema>;

Messages from client to server:

export const WebSocketInboundSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('sync'),
event: SyncEventSchema
}),
z.object({
type: z.literal('ping')
})
]);
export type WebSocketInbound = z.infer<typeof WebSocketInboundSchema>;

Messages from server to client:

export const WebSocketOutboundSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('connected'),
deviceId: z.string(),
timestamp: z.number()
}),
z.object({
type: z.literal('pong')
}),
z.object({
type: z.literal('sync'),
event: SyncEventSchema
}),
z.object({
type: z.literal('error'),
error: z.string()
})
]);
export type WebSocketOutbound = z.infer<typeof WebSocketOutboundSchema>;

export const D1BookmarkRowSchema = z.object({
id: z.string(),
user_id: z.string(),
encrypted_data: z.string(),
data_hash: z.string(),
created_at: z.number(),
updated_at: z.number(),
deleted_at: z.number().nullable()
});
export type D1BookmarkRow = z.infer<typeof D1BookmarkRowSchema>;
export const D1SyncLogRowSchema = z.object({
id: z.string(),
user_id: z.string(),
entity_type: z.string(),
entity_id: z.string(),
operation: z.string(),
timestamp: z.number(),
device_id: z.string(),
encrypted_data: z.string().nullable(),
data_hash: z.string(),
conflict_resolved: z.number(), // 0 or 1
resolution_strategy: z.string().nullable()
});
export type D1SyncLogRow = z.infer<typeof D1SyncLogRowSchema>;

export const UserSessionSchema = z.object({
user_id: z.string(),
email: z.string().email(),
name: z.string().optional(),
avatar_url: z.string().url().optional(),
email_verified: z.boolean()
});
export type UserSession = z.infer<typeof UserSessionSchema>;

export const ViewModeSchema = z.enum([
'table', // Spreadsheet-like table view
'cards', // Card grid view
'reader', // Reader/article view
'finder' // Finder/explorer view
]);
export type ViewMode = z.infer<typeof ViewModeSchema>;
export const SortOrderSchema = z.enum(['asc', 'desc']);
export type SortOrder = z.infer<typeof SortOrderSchema>;

export const GitHubOwnerSchema = z.object({
login: z.string(),
avatar_url: z.string().url()
});
export type GitHubOwner = z.infer<typeof GitHubOwnerSchema>;

import { BookmarkSchema, SyncEventSchema } from '@bitmarks.sh/core';
// Validate incoming data
const result = BookmarkSchema.safeParse(unknownData);
if (result.success) {
const bookmark = result.data; // Typed as Bookmark
} else {
console.error('Validation failed:', result.error);
}
import { z } from 'zod';
import { BookmarkSchema } from '@bitmarks.sh/core';
// Get TypeScript type from schema
type Bookmark = z.infer<typeof BookmarkSchema>;
// Use in function signatures
function processBookmark(bookmark: Bookmark) {
// ...
}
// For updates where not all fields are required
const BookmarkUpdateSchema = BookmarkSchema.partial();
// Only certain fields optional
const BookmarkCreateSchema = BookmarkSchema.omit({ id: true });

The API validates all incoming requests using these schemas:

// In route handler
app.post('/bookmarks', async (c) => {
const body = await c.req.json();
const result = EncryptedDataSchema.safeParse(body);
if (!result.success) {
return c.json({ error: 'Invalid request body' }, 400);
}
const { encrypted_data, data_hash } = result.data;
// ...
});