Server only
Database and private object storage
@silicon-jungle/inkwell-sdk/storageUse one D1 database and one game-scoped R2 namespace across all of this game’s players and worlds.
Exact source · npm package · Reference version 0.0.8
API at a glance
createRuntimeServices({ baseUrl?, token?, fetch?, timeoutMs? }?)database.query<Row>(sql, params?): Promise<DatabaseResult<Row>>database.batch<Row>(statements): Promise<DatabaseResult<Row>[]>storage.put(key, body, { contentType?, metadata?, worldId?, size?, signal? }?): Promise<StoredObjectInfo>storage.get(key, { signal? }?): Promise<Response>storage.delete(key, { signal? }?): Promise<void>storage.list({ prefix?, cursor?, limit? }?): Promise<StoredObjectList>InkwellDatabase, InkwellObjectStorage, RuntimeServiceError
Hosted handlers already receive these clients on context. createRuntimeServices reads INKWELL_RUNTIME_API_URL and INKWELL_RUNTIME_TOKEN by default; keep these server-only. It also supplies server game-service clients.
Use SQLite SQL and bound parameters. Batch accepts 1–20 statements with at most 100 parameters and 100,000 SQL characters each. Results include rows and optional database metadata.
Objects have safe relative keys up to 500 characters; no leading slash or path traversal. Streams need an explicit size. get throws RuntimeServiceError for HTTP failures, including a missing object; otherwise consume its Response body.
Follow cursor while truncated is true; list limit is 1–1,000. worldId labels quota accounting, not permission or a new database. Omit it for shared game-wide objects.
Private objects are never automatically client-readable. Serve authorized reads through your backend; never return a runtime token to the browser.
All exported symbols
DatabaseResultDatabaseStatementDatabaseValueInkwellDatabaseInkwellObjectStorageRuntimeServiceErrorStoredObjectInfoStoredObjectListcreateRuntimeServices
Exact TypeScript API
Generated from the SDK source, including input options, return values, public types and overloads. Relative imports below are links between SDK source modules, not additional package subpaths. Only the entrypoints listed in the reference index are supported import paths.
export type DatabaseValue = string | number | boolean | null;
export type DatabaseStatement = {
sql: string;
params?: DatabaseValue[];
};
export type DatabaseResult<Row extends Record<string, unknown> = Record<string, unknown>> = {
results: Row[];
meta: {
changedRows?: number;
lastRowId?: number;
rowsRead?: number;
rowsWritten?: number;
durationMs?: number;
};
};
export type StoredObjectInfo = {
key: string;
size: number;
etag: string;
uploadedAt: string;
contentType: string | null;
metadata: Record<string, string>;
};
export type StoredObjectList = {
objects: StoredObjectInfo[];
cursor: string | null;
truncated: boolean;
};
export declare class RuntimeServiceError extends Error {
readonly status: number;
readonly code: string;
constructor(status: number, code: string, message: string);
}
type RuntimeClientOptions = {
baseUrl: string;
token: string;
fetch?: typeof fetch;
timeoutMs?: number;
};
declare class RuntimeClient {
private readonly baseUrl;
private readonly token;
private readonly fetcher;
private readonly timeoutMs;
constructor(options: RuntimeClientOptions);
request(path: string, init?: RequestInit, timeoutMs?: number): Promise<Response>;
}
export declare class InkwellDatabase {
private readonly client;
constructor(client: RuntimeClient);
query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: DatabaseValue[]): Promise<DatabaseResult<Row>>;
batch<Row extends Record<string, unknown> = Record<string, unknown>>(statements: DatabaseStatement[]): Promise<DatabaseResult<Row>[]>;
}
export declare class InkwellObjectStorage {
private readonly client;
constructor(client: RuntimeClient);
put(key: string, value: BodyInit, options?: {
contentType?: string;
metadata?: Record<string, string>;
worldId?: string;
size?: number;
signal?: AbortSignal;
}): Promise<StoredObjectInfo>;
get(key: string, options?: {
signal?: AbortSignal;
}): Promise<Response>;
delete(key: string, options?: {
signal?: AbortSignal;
}): Promise<void>;
list(options?: {
prefix?: string;
cursor?: string;
limit?: number;
}): Promise<StoredObjectList>;
}
export declare function createRuntimeServices(options?: Partial<RuntimeClientOptions>): Readonly<{
database: InkwellDatabase;
presence: Readonly<{
get: () => Promise<import("./presence.js").BackendPresence>;
}>;
chat: Readonly<{
define: (name: string, options?: {
serverWritesOnly?: boolean;
}) => Promise<{
name: string;
serverWritesOnly: boolean;
}>;
channel(name?: string): Readonly<{
send: (body: string, options?: import("./chat.js").ChatSendOptions) => Promise<import("./chat.js").ChatReceipt>;
history: (after?: number) => Promise<import("./chat.js").ChatHistory>;
remove: (id: string) => Promise<{
success: true;
}>;
clear: () => Promise<{
success: true;
}>;
delete: () => Promise<{
success: true;
}>;
}>;
connect: (channel?: string, options?: import("./chat.js").ChatConnectOptions) => Promise<import("./chat.js").ChatConnection>;
list: () => Promise<{
channels: {
name: string;
serverWritesOnly: boolean;
}[];
}>;
}>;
achievements: Readonly<{
indicateProgressFor: (username: string, name: string, current: number, max: number, options?: {
locale?: string;
}) => Promise<{
displayed: boolean;
}>;
definitions: (offset?: number) => Promise<{
achievements: (import("./achievements.js").Achievement & import("./achievements.js").AchievementDefinition)[];
nextOffset: number | null;
}>;
define: (definition: import("./achievements.js").AchievementDefinition) => Promise<{
achievement: import("./achievements.js").AchievementDefinition;
}>;
update: (definition: import("./achievements.js").AchievementDefinition) => Promise<{
achievement: import("./achievements.js").AchievementDefinition;
}>;
delete: (name: string) => Promise<{
success: true;
}>;
unlockFor: (username: string, name: string) => Promise<import("./achievements.js").AchievementUnlock>;
clearFor: (username: string, name: string) => Promise<{
success: true;
}>;
onChange: typeof import("./achievements.js").onAchievementChange;
games: (options?: {
after?: string;
query?: string;
}) => Promise<{
games: {
slug: string;
title: string;
publisherUsername: string;
achievementCount: number;
}[];
nextCursor: string | null;
}>;
summary: (options?: Pick<import("./achievements.js").AchievementQuery, "game" | "username">) => Promise<{
total: number;
unlocked: number;
}>;
count: (options?: Pick<import("./achievements.js").AchievementQuery, "game">) => Promise<number>;
onNotification: typeof import("./achievements.js").onAchievementNotification;
indicateProgress: (name: string, current: number, max: number, options?: {
locale?: string;
}) => Promise<{
displayed: boolean;
}>;
list: (options?: import("./achievements.js").AchievementQuery) => Promise<{
achievements: import("./achievements.js").Achievement[];
nextOffset: number | null;
} & import("./offline.js").CachedGameRead>;
get: (name: string, options?: import("./achievements.js").AchievementQuery) => Promise<{
offline: boolean | undefined;
cachedAt: number | undefined;
pendingWrites: number | undefined;
name: string;
title: string;
description: string;
iconUrl: string | null;
hidden: boolean;
unlocked: boolean;
unlockedAt: string | null;
progress: {
current: number;
min: number;
target: number;
percent: number;
} | null;
} | null>;
unlock: (name: string) => Promise<import("./offline.js").QueuedGameWrite | import("./achievements.js").AchievementUnlock>;
clear: (name: string) => Promise<{
success: true;
}>;
percentage: (name: string, options?: {
game?: string;
}) => Promise<import("./achievements.js").AchievementPercentage | null>;
percentages: (options?: {
game?: string;
offset?: number;
}) => Promise<{
achievements: import("./achievements.js").AchievementPercentage[];
nextOffset: number | null;
}>;
}>;
stats: Readonly<{
batchFor: (username: string, changes: import("./stats.js").ProgressBatch, options?: import("./stats.js").ProgressBatchOptions) => Promise<import("./stats.js").ProgressBatchResult>;
definitions: (offset?: number) => Promise<{
stats: import("./stats.js").GameStatDefinition[];
nextOffset: number | null;
}>;
define: (definition: import("./stats.js").GameStatDefinition) => Promise<{
stat: import("./stats.js").GameStatDefinition;
}>;
update: (definition: import("./stats.js").GameStatDefinition) => Promise<{
stat: import("./stats.js").GameStatDefinition;
}>;
forPlayer: (username: string) => Readonly<{
batch: (changes: import("./stats.js").ProgressBatch, options?: import("./stats.js").ProgressBatchOptions) => Promise<import("./stats.js").ProgressBatchResult>;
onChange: typeof import("./stats.js").onStatChange;
schema: (options?: {
name?: string;
offset?: number;
}) => Promise<{
stats: import("./stats.js").GameStatSchema[];
nextOffset: number | null;
}>;
get: (name: string, options?: {
username?: string;
}) => Promise<{
offline: boolean | undefined;
cachedAt: number | undefined;
pendingWrites: number | undefined;
name: string;
title: string;
kind: "int" | "float" | "avgrate";
value: number;
updatedAt: string | null;
} | null>;
reset: (options?: {
achievements?: boolean;
}) => Promise<{
statsCleared: number;
achievementsCleared: number;
}>;
list: (options?: {
username?: string;
offset?: number;
}) => Promise<{
stats: import("./stats.js").GameStat[];
nextOffset: number | null;
} & import("./offline.js").CachedGameRead>;
set: (name: string, value: number, options?: {
requestId?: string;
}) => Promise<import("./offline.js").QueuedGameWrite | import("./stats.js").StatUpdate>;
increment: (name: string, amount?: number, options?: {
requestId?: string;
}) => Promise<import("./offline.js").QueuedGameWrite | import("./stats.js").StatUpdate>;
updateAverage: (name: string, count: number, seconds: number, options?: {
requestId?: string;
}) => Promise<import("./offline.js").QueuedGameWrite | import("./stats.js").StatUpdate>;
aggregate: (options?: import("./stats.js").AggregateGameStatQuery) => Promise<{
stats: import("./stats.js").AggregateGameStat[];
nextOffset: number | null;
}>;
}>;
onChange: typeof import("./stats.js").onStatChange;
schema: (options?: {
name?: string;
offset?: number;
}) => Promise<{
stats: import("./stats.js").GameStatSchema[];
nextOffset: number | null;
}>;
get: (name: string, options?: {
username?: string;
}) => Promise<{
offline: boolean | undefined;
cachedAt: number | undefined;
pendingWrites: number | undefined;
name: string;
title: string;
kind: "int" | "float" | "avgrate";
value: number;
updatedAt: string | null;
} | null>;
reset: (options?: {
achievements?: boolean;
}) => Promise<{
statsCleared: number;
achievementsCleared: number;
}>;
list: (options?: {
username?: string;
offset?: number;
}) => Promise<{
stats: import("./stats.js").GameStat[];
nextOffset: number | null;
} & import("./offline.js").CachedGameRead>;
set: (name: string, value: number, options?: {
requestId?: string;
}) => Promise<import("./offline.js").QueuedGameWrite | import("./stats.js").StatUpdate>;
increment: (name: string, amount?: number, options?: {
requestId?: string;
}) => Promise<import("./offline.js").QueuedGameWrite | import("./stats.js").StatUpdate>;
updateAverage: (name: string, count: number, seconds: number, options?: {
requestId?: string;
}) => Promise<import("./offline.js").QueuedGameWrite | import("./stats.js").StatUpdate>;
aggregate: (options?: import("./stats.js").AggregateGameStatQuery) => Promise<{
stats: import("./stats.js").AggregateGameStat[];
nextOffset: number | null;
}>;
}>;
storage: InkwellObjectStorage;
leaderboards: Readonly<{
board: (name: string) => import("./leaderboards.js").ServerLeaderboard;
find(name: string): Promise<import("./leaderboards.js").ServerLeaderboard | null>;
findOrCreate(definition: import("./leaderboards.js").LeaderboardDefinition): Promise<import("./leaderboards.js").ServerLeaderboard>;
define: (definition: import("./leaderboards.js").LeaderboardDefinition) => Promise<{
board: import("./leaderboards.js").LeaderboardInfo;
}>;
list: (offset?: number) => Promise<{
boards: import("./leaderboards.js").LeaderboardInfo[];
nextOffset: number | null;
}>;
}>;
}>;
export {};
Supporting internal type declarations
These clarify types referenced by the generated signatures; do not import these internal paths directly.
game-services.d.ts
export declare class GameServiceError extends Error {
readonly status: number;
readonly code: string;
constructor(message: string, status?: number, code?: string);
}
export type GameServiceRequest = <T>(service: string, request: Record<string, unknown>) => Promise<T>;
export declare const requestGameService: GameServiceRequest;
/** Server-only adapter. Credentials must never be bundled into browser games. */
export declare function createGameServiceRequest(options: {
baseUrl: string;
token: string;
fetch?: typeof fetch;
}): GameServiceRequest;
protocol.d.ts
export declare const SDK_SOURCE: "inkwell-sdk";
export declare const SDK_VERSION: 1;
export type InkwellMessageType = "ready" | "loading.progress" | "loading.error" | "session.complete" | "analytics.track" | "assets.progress" | "player.get" | "presence.get" | "performance.sample" | "backend.connect" | "backend.fetch";
export type InkwellMessage = {
source: typeof SDK_SOURCE;
version: typeof SDK_VERSION;
type: InkwellMessageType;
payload?: Record<string, unknown>;
sentAt: number;
};
export declare function parentOrigin(): string | null;
export declare function requestId(): string;
export declare function emit(type: InkwellMessageType, payload?: Record<string, unknown>): boolean;