npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mathrunet/masamune_cloudflare_turso

v3.7.6

Published

Server-side package of the Masamune framework for working with Turso via Cloudflare.

Readme


[GitHub] | [YouTube] | [Packages] | [X] | [LinkedIn] | [mathru.net]


Just load the package in index.ts and pass the predefined data to the methods to implement the server side.

Also, masamune_functions_cloudflare can be used to execute server-side functions from methods defined on the client side, allowing for safe implementation.

Installation

Install the following packages

npm install @mathrunet/masamune_cloudflare_turso

Implementation

Before deploying, enable Concurrent Writes in Turso Dashboard under Settings > General. Masamune provisions only the new TursoDB (MVCC) engine; it never silently falls back to the legacy SQLite engine.

Pass the return value of the deploy function to export default. It is defined by passing various Workers to the deploy function.

import * as m from "@mathrunet/masamune_cloudflare_turso";
import rulesJson from "../rules.json";

export default m.deploy(
  [
    m.Functions.turso({
      organization: "xxxx",
      group: "xxxx",
      autoCreateDatabase: true,
      autoCreateTable: true,
      autoMigrateAddColumns: true,
    }),
    m.Functions.tursoToken({
      organization: "xxxx",
      group: "xxxx",
      autoCreateDatabase: true,
    }),
  ],
  {
    rules: rulesJson,
  },
);

Cloudflare bindings are also supported and take precedence over options:

  • TURSO_PLATFORM_API_TOKEN
  • TURSO_ORGANIZATION
  • TURSO_GROUP
  • TURSO_GROUPS (a JSON array of multiple groups)
  • TURSO_SERVER_TOKEN_TTL_SECONDS (default: 3600)

For production, store the Platform API token as a secret:

wrangler secret put TURSO_PLATFORM_API_TOKEN

Hosted Turso does not accept arbitrary self-signed Ed25519 JWT keys. Both client tokens and Worker-side database tokens therefore use the official Platform API. Worker-side full-access tokens are always issued with an expiration, cached only until 60 seconds before expiry, and refreshed with a single in-flight request per database. The old unbounded server token behavior is not used.

katana apply configures the server TTL with cloudflare.turso.server_token_ttl. Set cloudflare.turso.rotate_legacy_tokens: true only when you intentionally want to invalidate all previously issued tokens in the Turso group.

既存DBへの環境別対応表

databaseBindings は、モデルの論理DB名と既存TursoDBの名前が異なる場合に、 Workerの FLAVOR ごとに接続先を固定します。prefixを送らない旧Dartクライアントでも、 モデルやrulesの論理パスを変更せず利用できます。

const existingDatabase: turso.TursoWorkersOptions = {
  databaseBindings: {
    dev: {
      main: { database: "example-dev-main", group: "example-dev" },
    },
  },
  autoCreateDatabase: false,
  autoCreateTable: false,
  autoMigrateAddColumns: false,
};

// rulesはdeploy側、または各Functionのoptionsへ従来どおり指定します。
export default m.deploy([
  turso.Functions.turso(existingDatabase),
  turso.Functions.tursoToken(existingDatabase),
], { rules });

上記は FLAVOR=dev、論理DB main、prefixなしの要求だけを許可します。 FLAVOR の省略、prodや未登録DBへの要求、クライアントprefixの指定は拒否されます。 databasePrefix との併用や、別の論理名・環境への同一物理DBの重複登録も拒否します。 対応表はクライアント入力から生成せず、サーバー管理の設定に限定してください。

対応表を使う接続は autoCreateDatabase: true が上流に残っていてもDBを作成しません。 Platform APIから取得したDBのgroupが対応表と一致しない場合も拒否します。 接続キャッシュは環境・物理DB・groupで分離し、rulesとschemaは元の論理DB名で評価します。 この設定はWorker用です。環境を確定していない TursoDatabaseAdapter への直接指定は拒否します。

Workerへ個人のCLIログイントークンを転送せず、対象organization/groupに限定した Platform API tokenを設定してください。既存DBの参照とSQL token発行には read と db:mint-token を指定します。新しいtokenの作成は権限管理者の運用で行い、 token値はログやソースに保存しません。対応表はSQLのread-only権限を付与する機能ではないため、 読み取り制限は既存のrulesで指定します。

Worker placement

Register Turso in the edge Worker entry (src/edge.ts) and do not set placement on that Worker. Cloudflare runs a Worker without placement in the data center that receives the request, so a new database is created in the group nearest to the client and later requests reach it from a nearby Worker. Smart Placement or a placement hint moves the whole Worker to one location and adds distance for users in other regions. Keep fixed-region databases such as TiDB in a separate region Worker. See "Edge and Region Workers" in the @mathrunet/masamune_cloudflare README.

The Worker closes the Hrana stream after sending the response when ExecutionContext.waitUntil is available.

Multiple groups and automatic region selection

Register existing Turso groups for each region to let the Worker choose where to create a new database without requiring Flutter to specify a group. This does not create or move groups. The Platform API token must allow retrieving and creating databases and issuing database-scoped tokens for every configured group.

import * as turso from "@mathrunet/masamune_cloudflare_turso";
import rulesJson from "../rules.json";

const tursoOptions: turso.TursoWorkersOptions = {
  organization: "my-organization",
  group: "prod-apac", // Default for unknown regions; otherwise the first entry in groups.
  groups: [
    { name: "prod-apac", countries: ["JP"], continents: ["AS", "OC"] },
    { name: "prod-us", continents: ["NA", "SA"] },
    { name: "prod-eu", continents: ["EU", "AF"] },
  ],
  autoCreateDatabase: true,
};

export default turso.deploy([
  turso.Functions.turso(tursoOptions),
  turso.Functions.tursoToken(tursoOptions),
], { rules: rulesJson });

Placement uses the following priority order. Region information comes from Cloudflare-provided request.cf.country and request.cf.continent.

  1. The group returned by the server's resolveGroup callback
  2. The client's group preference (only values listed in groups are allowed)
  3. A match in countries
  4. A match in continents
  5. The default group, or the first entry in groups when omitted

Selection uses configured region mappings; it does not measure latency on every request. Assigning the same country or continent to multiple groups is rejected. A country match takes precedence over a continent match.

Existing databases are resolved by organization and physical database name, using the group and connection URL returned by the API. They are not moved or duplicated when a user travels or requests another group. Paths such as database/user-abc/items/one work unchanged for databases in different groups. Groups do not namespace database names: physical names still derive from the prefix and logical database name. This does not provide queries or transactions across groups.

When groups is configured, it also acts as an allowlist for the actual group of existing databases, including cached connections and concurrent resolution. Legacy configurations with only group retain a single creation destination and do not permit clients to override it.

Custom placement policies and non-HTTP calls

const options: turso.TursoWorkersOptions = {
  ...tursoOptions,
  resolveGroup: async ({ database, databasePrefix, authentication, requestedGroup, country, continent, groups }) => {
    // Return a name in groups according to the app's placement constraints.
    // Return undefined to continue with client preference, region mapping, and defaults.
    return undefined;
  },
};
const adapter = new turso.TursoDatabaseAdapter({
  options,
  groupContext: { country: "JP", continent: "AS" },
});

The resolver runs only when creating a new database. Authorize existing databases using rules and the group allowlist. Non-HTTP calls without region information fall back to the default. A validated client preference remains a placement hint, not authorization data.

Direct-connection token responses include group and primaryRegion when available. They are omitted from responses that do not resolve a database, such as functions-only responses. Only database-scoped tokens are returned for direct connections.

Katana CLI configuration

cloudflare:
  turso:
    enable: true
    organization: my-organization
    group:
      dev: dev-apac
      prod: prod-apac
    groups:
      dev:
        - name: dev-apac
          location: aws-ap-northeast-1
      prod:
        - name: prod-apac
          location: aws-ap-northeast-1
          continents: [AS, OC]
        - name: prod-us
          location: aws-us-east-1
          continents: [NA, SA]
        - name: prod-eu
          location: aws-eu-west-1
          continents: [EU, AF]

When a group has location, katana apply creates the group through the Platform API if it does not exist. Existing groups are not changed. Creating groups requires an organization-wide Platform API token, and multiple groups require a Turso plan that supports them. location is not written to TURSO_GROUPS.

katana apply writes environment-specific TURSO_GROUPS (a JSON string) to Wrangler. TURSO_GROUPS takes precedence over options.groups. TURSO_GROUP overrides only the default, not region selection or the resolver. With multiple groups, the default must also appear in the list. Configuring only groups is supported.

Reapplying preserves custom options and references to shared configuration in edge.ts. When passing all options through a variable, as in turso.Functions.turso(sharedOptions), manage autoCreateDatabase and schemaManifest in that variable as well. Setting rotate_legacy_tokens: true targets all configured groups.

Endpoints

The package exposes Turso through a single provider path.

| Method | Path | Description | | -------- | -------------- | --------------------------------- | | GET | /turso/database/{database}/{table} | Read rows or count rows. | | GET | /turso/database/{database}/{table}/{indexKey} | Read a row. | | POST | /turso/database/{database}/{table} | Create a row. | | POST | /turso/database/{database}/{table}/{indexKey} | Create a row with an explicit ID. | | PUT | /turso/database/{database}/{table} | Update rows. | | PUT | /turso/database/{database}/{table}/{indexKey} | Update a row. | | DELETE | /turso/database/{database}/{table} | Delete rows. | | DELETE | /turso/database/{database}/{table}/{indexKey} | Delete a row. | | POST | /turso/token/database/{database} | Issue a database-scoped short-lived token. |

GET uses the path for database, table, and optional indexKey.

/turso/database/main/users/user_1

Collection queries can pass where, orderBy, and limit.

/turso/database/main/users?where=[{"type":"equalTo","key":"name","value":"Alice"}]&orderBy=[{"key":"created_at","descending":true}]&limit=20

Clients may pass prefix to select a separate physical database while keeping the logical path used by rules unchanged. Prefixes are trimmed, trailing underscores are removed, and exactly one underscore is appended.

/turso/database/main/users?prefix=dev

This request authorizes main/users and connects to dev_main. The token endpoint accepts the same value in its JSON body, so CRUD and direct tokens always resolve the same physical database. A missing, empty, or underscore-only prefix keeps the existing database name.

Supported where types are equalTo, notEqualTo, lessThan, lessThanOrEqualTo, greaterThan, greaterThanOrEqualTo, whereIn, whereNotIn, isNull, isNotNull, and like.

POST / PUT / DELETE use the same path format and JSON bodies for value or query filters.

{
  "value": {
    "name": "Alice"
  }
}

The previous query/body style is still accepted for compatibility:

/turso?database=main&table=users&indexKey=user_1

Database and schema management

The worker can create TursoDB databases and tables automatically. Platform API creation requests always include use_tursodb: true.

If a database with the requested name already exists, its database ID is validated using the same TursoDB marker as the Turso CLI. A legacy SQLite database, or a response whose engine cannot be verified, is rejected. Existing SQLite databases cannot be converted in place; create a replacement and migrate the data before using the same logical database path.

turso db create --tursodb <new-database-name>

Database creation is disabled by default. Set autoCreateDatabase: true explicitly on every Turso function or adapter that is allowed to provision a database. A missing database otherwise returns 404 and is never created.

m.Functions.turso({
  organization: TURSO_ORGANIZATION,
  group: TURSO_GROUP,
  platformApiToken: TURSO_PLATFORM_API_TOKEN,
  autoCreateDatabase: true,
  autoCreateTable: true,
  autoMigrateAddColumns: true,
});

Worker-side integrations such as Purchase and Notification use TursoDatabaseAdapter. It follows the same full-path contract as the CRUD endpoints; the logical database name must be included in every path.

const database = new m.TursoDatabaseAdapter({
  options: {
    organization: TURSO_ORGANIZATION,
    group: TURSO_GROUP,
    platformApiToken: TURSO_PLATFORM_API_TOKEN,
    autoCreateDatabase: true,
  },
});

await database.getDocument("database/user-1/userProfiles/user-1");
await database.query("database/user-1/capturedMonsters");

Paths without the database/{database_name}/ prefix are rejected before a Turso connection is resolved. TursoDatabaseAdapter has no fixed database constructor option and never falls back to main.

Application database paths use a logical database name. When that name already matches Turso's physical naming rules (lower-case letters, numbers, and hyphens, up to 56 characters), it is used unchanged. Other logical names, including mixed-case Firebase UIDs, are mapped deterministically to a lower-case physical name before calling the Turso Platform API. Rules continue to evaluate the original logical name, so a rule such as {"type":"path","param":"uid"} still compares the authenticated Firebase UID without weakening authorization.

groupName must point to an existing Turso group. The region/location is set when the group is created, not when each database is created.

turso group create my-group --location aws-ap-northeast-1

groupName can also be omitted when the runtime environment provides TURSO_GROUP_NAME, such as through a local .env file or Cloudflare Worker environment variables. When neither groupName nor TURSO_GROUP_NAME is configured, database access that needs automatic database creation returns an error.

Automatic migration is intentionally limited to additive field changes.

  • New fields in value are added with ALTER TABLE ADD COLUMN.
  • A schemaManifest applies all declared columns before reads as well as writes, so a newly released filter/order does not race the first write.
  • Direct-read token issuance applies the requested tables first, so Flutter direct reads cannot bypass the migration.
  • Concurrent ADD COLUMN attempts are idempotent; a duplicate-column result is re-read and accepted only when the resulting type is compatible.
  • A missing column whose first value is null is rejected instead of being permanently inferred as TEXT. Supply a generated schema manifest first.
  • Existing fields are not migrated when their inferred type changes.
  • Field rename, field deletion, primary key changes, unique constraints, and foreign keys are not automatically migrated.
  • PUT and DELETE require indexKey or where to avoid accidental full-table changes.

Worker-side POST, PUT, and DELETE operations use the libSQL-compatible @tursodatabase/serverless/compat client and a BEGIN CONCURRENT transaction on that client's session. Row conflicts and SQLITE_BUSY at commit are rolled back and retried with bounded backoff; constraints and other SQL errors are returned without retrying.

The default table shape is:

CREATE TABLE IF NOT EXISTS table_name (
  id TEXT PRIMARY KEY,
  created_at INTEGER,
  updated_at INTEGER,
  ...
)

Objects and arrays are stored as JSON strings.

Generated manifests have the following shape. database accepts an exact logical database, *, or a :parameter pattern for per-user Turso databases.

turso.Functions.turso({
  schemaManifest: {
    version: "1-a1b2c3d4",
    tables: {
      users: {
        database: "*",
        table: "users",
        columns: [
          { name: "name", type: "TEXT" },
          { name: "age", type: "BIGINT" },
        ],
      },
    },
  },
});

For releases, add nullable columns first, deploy the schema-aware Worker, then release the app. Renames and required/type changes use dual-write, backfill, read cutover, and delayed cleanup; automatic migration never drops columns.

Rules

Rules are provided by @mathrunet/masamune_cloudflare and are shared with other Cloudflare database packages. Pass an imported rules.json to WorkersOptions.rules on deploy, or pass the same config to each function option.

{
  "version": "1",
  "rules": {
    "database": {
      "main": {
        "read": "allow",
        "write": "server"
      },
      "main/users/*": {
        "read": "authenticated",
        "write": { "type": "field", "field": "ownerId", "server": true }
      }
    }
  }
}

Database-scoped short-lived token

Use /turso/token/database/{database} to issue a Turso database token after resolving read-only or full-access authorization through rules.

Token authorization is evaluated against the database path:

{
  "version": "1",
  "rules": {
    "database": {
      "main": {
        "read": "authenticated",
        "write": "deny"
      }
    }
  }
}

If read is allowed and write is denied, the worker issues a read-only token. If both read and all write operations are allowed, the worker issues a full-access token. If read is denied, the token request returns 403.

Named path parameters can be compared with the authenticated user ID:

{
  "version": "1",
  "rules": {
    "database": {
      "{uid}": {
        "read": { "type": "path", "param": "uid" },
        "write": { "type": "path", "param": "uid" }
      }
    }
  }
}

With this rule, only the authenticated user whose uid is equal to the database name can access that database.

Read and write operations can be restricted to the Workers endpoint while issuing only the direct Turso token that is safe for the requested scope:

{
  "version": "1",
  "rules": {
    "database": {
      "{uid}": {
        "read": { "type": "path", "param": "uid" },
        "write": {
          "type": "path",
          "param": "uid",
          "server": true
        }
      }
    }
  }
}

server can also be used with read and field rules. A server-side rule does not grant direct token access for that operation.

Table/document rules below the database path are also treated as server-side rules for token issuance when they restrict read or write. This is intentional because Turso Platform tokens are database-scoped and cannot enforce document-level field or path checks on the client.

For a database-level Turso token, send operations without a table:

{
  "ttlSeconds": 600,
  "operations": ["read"]
}

When the client also needs the Workers backend to decide whether a specific table must use Functions fallback, send targets. targets are used only for Masamune rules resolution; they are not passed to Turso as token scopes.

{
  "ttlSeconds": 600,
  "targets": [
    {
      "table": "users",
      "operations": ["read", "write"]
    }
  ]
}

The response is:

{
  "token": "<jwt>",
  "expiresAt": 1760000000,
  "url": "libsql://your-db.turso.io",
  "readMode": "direct",
  "writeMode": "functions",
  "targets": [
    {
      "table": "users",
      "operations": ["read", "write"],
      "readMode": "direct",
      "writeMode": "functions"
    }
  ]
}

If both read and write are functions-only, /turso/token/database/{database} does not return token, expiresAt, or url; it returns only the resolved modes and targets:

{
  "readMode": "functions",
  "writeMode": "functions",
  "targets": [
    {
      "table": "users",
      "operations": ["read", "write"],
      "readMode": "functions",
      "writeMode": "functions"
    }
  ]
}

The previous scope request field and scopes response field are still accepted/returned for compatibility, but new clients should use operations and targets.

url is resolved by the Workers backend. This lets clients use direct libSQL access for dynamically created databases without building the Turso hostname on the client.

ttlSeconds defaults to 600 seconds and is capped by maxTtlSeconds (default: 3600 seconds).

Tokens are generated with the Turso Platform API:

POST /v1/organizations/{organizationSlug}/databases/{databaseName}/auth/tokens

Native vectors

Fixed-dimension vectors declared in the schema support saves, updates, deletes, and nearest queries. Supported metrics are cosine (default) and euclidean, dimensions range from 1 to 16383, and query limits range from 1 to 100 (default 10). Values must be finite float32 numbers and match the declared dimensions and metric. Zero vectors are rejected for cosine distance.

The GET nearest parameter is a JSON string such as {"key":"embedding","value":[1,0,0]}. The database applies ordinary where conditions and sorts by distance, breaking ties by ID. Document rules are reevaluated for each candidate. Authorization filtering can produce fewer results than the limit. Combining nearest with count, orderBy, or a document ID is rejected, as is nearest on mutation requests.

Writes accept arrays or ModelVectorValue JSON; reads return @type, @source, @vector, and @measure. An unloaded value represented by @vector: [], or an omitted vector column, preserves the existing value. Explicit null clears it. Existing TEXT columns are not implicitly converted to native vectors. Plan a separate column migration and data conversion first.

TursoDB manifests use type: "F32_BLOB(3)" and optionally vectorMetric: "euclidean". Queries use vector32, vector_extract, and vector_distance_cos or vector_distance_l2. This integration uses an exact scan because the target TursoDB implementation does not support vector indexes; it does not use legacy libSQL vector_top_k or libsql_vector_idx. Flutter routes all CRUD through the schema-aware Worker with nativeVectors: true. CRUD, authorization, and rollback have been verified against a real TursoDB instance through a Worker.

GitHub Sponsors

Sponsors are always welcome. Thank you for your support!

https://github.com/sponsors/mathrunet