Google Cloud Spanner storage
The Google Cloud Spanner storage implementation provides a horizontally scalable, strongly consistent storage backend for Mastra. It targets the GoogleSQL dialect of Cloud Spanner.
InstallationDirect link to Installation
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/spanner@latest
pnpm add @mastra/spanner@latest
yarn add @mastra/spanner@latest
bun add @mastra/spanner@latest
UsageDirect link to Usage
import { SpannerStore } from '@mastra/spanner'
const storage = new SpannerStore({
id: 'spanner-storage',
projectId: process.env.SPANNER_PROJECT_ID!,
instanceId: process.env.SPANNER_INSTANCE_ID!,
databaseId: process.env.SPANNER_DATABASE_ID!,
})
The instance and database must already exist. The adapter creates the required tables on first use, so the credentials provided to the Spanner client need permission to run schema changes (or run storage.init() once during a deploy step with elevated credentials).
ParametersDirect link to Parameters
id:
projectId?:
instanceId?:
databaseId?:
database?:
spannerOptions?:
disableInit?:
skipDefaultIndexes?:
indexes?:
initMode?:
Constructor examplesDirect link to Constructor examples
You can instantiate SpannerStore in several ways:
import { Spanner } from '@google-cloud/spanner'
import { SpannerStore } from '@mastra/spanner'
// Using projectId / instanceId / databaseId
const store1 = new SpannerStore({
id: 'spanner-storage-1',
projectId: 'my-gcp-project',
instanceId: 'my-instance',
databaseId: 'mastra',
})
// Reusing an existing Spanner Database handle
const spanner = new Spanner({ projectId: 'my-gcp-project' })
const database = spanner.instance('my-instance').database('mastra')
const store2 = new SpannerStore({
id: 'spanner-storage-2',
database,
})
// Using the local Spanner emulator (set the SPANNER_EMULATOR_HOST env var)
process.env.SPANNER_EMULATOR_HOST = 'localhost:9010'
const store3 = new SpannerStore({
id: 'spanner-storage-emulator',
projectId: 'test-project',
instanceId: 'test-instance',
databaseId: 'test-db',
spannerOptions: { servicePath: 'localhost', port: 9010, sslCreds: undefined },
})
Additional notesDirect link to Additional notes
Schema managementDirect link to Schema management
The storage adapter creates the following tables, all using the GoogleSQL dialect:
mastra_workflow_snapshot: workflow state and execution datamastra_threads: conversation threadsmastra_messages: individual messagesmastra_resources: resource working memorymastra_scorers: evaluation scoresmastra_background_tasks: background tool execution statemastra_agents: thin agent records (id, status, active version)mastra_agent_versions: versioned agent configuration snapshotsmastra_mcp_clients/mastra_mcp_client_versions: MCP client configurations and their version historymastra_mcp_servers/mastra_mcp_server_versions: MCP server configurations and their version historymastra_skills/mastra_skill_versions: skill records and versioned skill snapshots (instructions, references, scripts, assets, content tree)mastra_skill_blobs: content-addressable blob store keyed by SHA-256 hash, used for skill version contentsmastra_prompt_blocks/mastra_prompt_block_versions: prompt block records and versioned content snapshots (template content, rules, request-context schema)mastra_scorer_definitions/mastra_scorer_definition_versions: scorer definition records and versioned config snapshots (judge instructions, model, score range, preset config, default sampling)mastra_schedules/mastra_schedule_triggers: cron-driven workflow schedules and trigger history, consumed by Mastra's built-inWorkflowSchedulermastra_ai_spans: AI tracing spans for observability (per-trace and per-span records, used to power the Studio traces UI)
Tables are created with STRING(MAX) for text and JSON payloads, INT64, FLOAT64, BOOL, and TIMESTAMP.
Two tables also carry Spanner-specific STORED generated columns that the adapter populates from JSON payloads so common filters can use a regular secondary index instead of a JSON_VALUE scan:
mastra_workflow_snapshot.snapshotStatus— extracts$.statusfromsnapshot; backslistWorkflowRuns({ status }).mastra_schedules.target_workflow_id— extracts$.workflowIdfromtarget; backslistSchedules({ workflowId }).
Both are added via ALTER TABLE ... ADD COLUMN IF NOT EXISTS during init() and skipped under initMode: 'validate' (where the schema is owned externally). When the column is absent, the adapter falls back to a JSON_VALUE filter at runtime.
The adapter does not create or use named schemas; use a dedicated database for isolation.
InitializationDirect link to Initialization
When you pass storage to the Mastra class, init() is called automatically before any storage operation:
import { Mastra } from '@mastra/core'
import { SpannerStore } from '@mastra/spanner'
const storage = new SpannerStore({
id: 'spanner-storage',
projectId: process.env.SPANNER_PROJECT_ID!,
instanceId: process.env.SPANNER_INSTANCE_ID!,
databaseId: process.env.SPANNER_DATABASE_ID!,
})
const mastra = new Mastra({
storage, // init() is called automatically
})
If you use storage directly, call init() once before the first operation. Spanner does not allow concurrent schema changes, so SpannerStore.init() runs each domain's setup sequentially.
const storage = new SpannerStore({
id: 'spanner-storage',
projectId: process.env.SPANNER_PROJECT_ID!,
instanceId: process.env.SPANNER_INSTANCE_ID!,
databaseId: process.env.SPANNER_DATABASE_ID!,
})
await storage.init()
const memory = await storage.getStore('memory')
const thread = await memory?.getThreadById({ threadId: '...' })
If init() is not called and disableInit is true, the required tables will not exist and storage operations will fail.
GoogleSQL specificsDirect link to GoogleSQL specifics
A few behaviors differ from other relational adapters:
- Upserts use
INSERT OR UPDATE. Spanner does not provide aRETURNINGclause for upserts, so callers needing the post-write state must read it back. - There is no
TRUNCATE;dangerouslyClearAll()issuesDELETE WHERE TRUE. - Identifiers are quoted with backticks.
- DDL is applied through
database.updateSchema(...), which is asynchronous (long-running operation). NULLS FIRST/LASTis not supported. Ordering with NULL handling is emulated through anIS NULLordering key.- JSON containment is not supported natively.
listTracesmetadataandscopefilters compile to per-keyJSON_VALUE(...) = @vequality checks, andtagsfilters compile toEXISTSoverJSON_QUERY_ARRAY(...). This differs from Postgres'@>containment operator (which can match nested structure in a single index scan) — most one-shot lookups still work but deeply nested structural matches are not expressible.
Direct database accessDirect link to Direct database access
SpannerStore exposes the underlying Spanner client objects:
store.database // @google-cloud/spanner Database
store.instance // @google-cloud/spanner Instance (when created internally)
store.spanner // @google-cloud/spanner Spanner client (when created internally)
These are intended for advanced scenarios such as bespoke transactions or schema introspection. When you reuse the database directly, you bypass the adapter's validation and JSON conversion logic.
Local development with the emulatorDirect link to Local development with the emulator
Run the Cloud Spanner emulator locally with Docker:
docker run -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
Set SPANNER_EMULATOR_HOST=localhost:9010 and create the instance and database before running your app:
gcloud spanner instances create test-instance --config=emulator-config --nodes=1
gcloud spanner databases create test-db --instance=test-instance
Then connect with the same env var set in your Node.js process; the @google-cloud/spanner client detects the emulator automatically.