rowid,title,content,sections_fts,rank 1,Configuration,"Datasette offers several ways to configure your Datasette instances: server settings, plugin configuration, authentication, and more. Most configuration can be handled using a datasette.yaml configuration file, passed to datasette using the -c/--config flag: datasette mydatabase.db --config datasette.yaml This file can also use JSON, as datasette.json . YAML is recommended over JSON due to its support for comments and multi-line strings.",27, 2,Configuration via the command-line,"The recommended way to configure Datasette is using a datasette.yaml file passed to -c/--config . You can also pass individual settings to Datasette using the -s/--setting option, which can be used multiple times: datasette mydatabase.db \ --setting settings.default_page_size 50 \ --setting settings.sql_time_limit_ms 3500 This option takes dotted-notation for the first argument and a value for the second argument. This means you can use it to set any configuration value that would be valid in a datasette.yaml file. It also works for plugin configuration, for example for datasette-cluster-map : datasette mydatabase.db \ --setting plugins.datasette-cluster-map.latitude_column xlat \ --setting plugins.datasette-cluster-map.longitude_column xlon If the value you provide is a valid JSON object or list it will be treated as nested data, allowing you to configure plugins that accept lists such as datasette-proxy-url : datasette mydatabase.db \ -s plugins.datasette-proxy-url.paths '[{""path"": ""/proxy"", ""backend"": ""http://example.com/""}]' This is equivalent to a datasette.yaml file containing the following: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" plugins: datasette-proxy-url: paths: - path: /proxy backend: http://example.com/ """""").strip() ) ]]] [[[end]]]",27, 3,,"The following example shows some of the valid configuration options that can exist inside datasette.yaml . [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # Datasette settings block settings: default_page_size: 50 sql_time_limit_ms: 3500 max_returned_rows: 2000 # top-level plugin configuration plugins: datasette-my-plugin: key: valueA # Database and table-level configuration databases: your_db_name: # plugin configuration for the your_db_name database plugins: datasette-my-plugin: key: valueA tables: your_table_name: allow: # Only the root user can access this table id: root # plugin configuration for the your_table_name table # inside your_db_name database plugins: datasette-my-plugin: key: valueB """""") ) ]]] [[[end]]]",27, 4,Settings,"Settings can be configured in datasette.yaml with the settings key: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # inside datasette.yaml settings: default_allow_sql: off default_page_size: 50 """""").strip() ) ]]] [[[end]]] The full list of settings is available in the settings documentation . Settings can also be passed to Datasette using one or more --setting name value command line options.`",27, 5,Plugin configuration,"Datasette plugins often require configuration. This plugin configuration should be placed in plugins keys inside datasette.yaml . Most plugins are configured at the top-level of the file, using the plugins key: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # inside datasette.yaml plugins: datasette-my-plugin: key: my_value """""").strip() ) ]]] [[[end]]] Some plugins can be configured at the database or table level. These should use a plugins key nested under the appropriate place within the databases object: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # inside datasette.yaml databases: my_database: # plugin configuration for the my_database database plugins: datasette-my-plugin: key: my_value my_other_database: tables: my_table: # plugin configuration for the my_table table inside the my_other_database database plugins: datasette-my-plugin: key: my_value """""").strip() ) ]]] [[[end]]]",27, 6,Permissions configuration,"Datasette's authentication and permissions system can also be configured using datasette.yaml . Here is a simple example: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # Instance is only available to users 'sharon' and 'percy': allow: id: - sharon - percy # Only 'percy' is allowed access to the accounting database: databases: accounting: allow: id: percy """""").strip() ) ]]] [[[end]]] Access permissions in datasette.yaml has the full details.",27, 7,Queries configuration,"Queries are named SQL queries that appear in the Datasette interface. They can be configured in datasette.yaml using the queries key at the database level: [[[cog from metadata_doc import config_example, config_example config_example(cog, { ""databases"": { ""sf-trees"": { ""queries"": { ""just_species"": { ""sql"": ""select qSpecies from Street_Tree_List"" } } } } }) ]]] [[[end]]] See the queries documentation for more, including how to configure writable queries .",27, 8,Custom CSS and JavaScript,"Datasette can load additional CSS and JavaScript files, configured in datasette.yaml like this: [[[cog from metadata_doc import config_example config_example(cog, """""" extra_css_urls: - https://simonwillison.net/static/css/all.bf8cd891642c.css extra_js_urls: - https://code.jquery.com/jquery-3.2.1.slim.min.js """""") ]]] [[[end]]] The extra CSS and JavaScript files will be linked in the of every page: You can also specify a SRI (subresource integrity hash) for these assets: [[[cog config_example(cog, """""" extra_css_urls: - url: https://simonwillison.net/static/css/all.bf8cd891642c.css sri: sha384-9qIZekWUyjCyDIf2YK1FRoKiPJq4PHt6tp/ulnuuyRBvazd0hG7pWbE99zvwSznI extra_js_urls: - url: https://code.jquery.com/jquery-3.2.1.slim.min.js sri: sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g= """""") ]]] [[[end]]] This will produce: Modern browsers will only execute the stylesheet or JavaScript if the SRI hash matches the content served. You can generate hashes using www.srihash.org Items in ""extra_js_urls"" can specify ""module"": true if they reference JavaScript that uses JavaScript modules . This configuration: [[[cog config_example(cog, """""" extra_js_urls: - url: https://example.datasette.io/module.js module: true """""") ]]] [[[end]]] Will produce this HTML: ",27, 9,Table configuration,Datasette supports a number of table-level configuration options inside datasette.yaml . These are placed under databases.database_name.tables.table_name .,27, 10,,"By default Datasette tables are sorted by primary key. You can set a default sort order for a specific table using the sort or sort_desc properties: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: sort: created """""").strip() ) ]]] [[[end]]] Or use sort_desc to sort in descending order: [[[cog config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: sort_desc: created """""").strip() ) ]]] [[[end]]]",27, 11,,"Datasette defaults to displaying 100 rows per page, for both tables and views. You can change this on a per-table or per-view basis using the size key: [[[cog config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: size: 10 """""").strip() ) ]]] [[[end]]] This size can still be over-ridden by passing e.g. ?_size=50 in the query string.",27, 12,,"Datasette allows any column to be used for sorting by default. If you need to control which columns are available for sorting you can do so using sortable_columns : [[[cog config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: sortable_columns: - height - weight """""").strip() ) ]]] [[[end]]] This will restrict sorting of example_table to just the height and weight columns. You can also disable sorting entirely by setting ""sortable_columns"": [] You can use sortable_columns to enable specific sort orders for a view called name_of_view in the database my_database like so: [[[cog config_example(cog, textwrap.dedent( """""" databases: my_database: tables: name_of_view: sortable_columns: - clicks - impressions """""").strip() ) ]]] [[[end]]]",27, 13,,"Datasette's HTML interface attempts to display foreign key references as labelled hyperlinks. By default, it automatically detects a label column using the following rules (in order): If there is exactly one unique text column, use that. If there is a column called name or title (case-insensitive), use that. If the table has only two columns - a primary key and one other - use the non-primary-key column. You can override this automatic detection by specifying which column should be used for the link label with the label_column property: [[[cog config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: label_column: title """""").strip() ) ]]] [[[end]]]",27, 14,,"You can hide tables from the database listing view (in the same way that FTS and SpatiaLite tables are automatically hidden) using ""hidden"": true : [[[cog config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: hidden: true """""").strip() ) ]]] [[[end]]]",27, 15,,"You can turn on facets by default for specific tables. facet_size controls how many unique values are shown for each facet on that table (the default is controlled by the default_facet_size setting). See Facets in configuration for full details. [[[cog config_example(cog, textwrap.dedent( """""" databases: sf-trees: tables: Street_Tree_List: facets: - qLegalStatus facet_size: 10 """""").strip() ) ]]] [[[end]]] You can also specify array or date facets using JSON objects with a single key of array or date : facets: - array: tags - date: created",27, 16,,"These configure full-text search for a table or view. See Configuring full-text search for a table or view for full details. fts_table specifies which FTS table to use for search. fts_pk sets the primary key column if it is something other than rowid . searchmode can be set to ""raw"" to enable SQLite advanced search operators . [[[cog config_example(cog, textwrap.dedent( """""" databases: russian-ads: tables: display_ads: fts_table: ads_fts fts_pk: id searchmode: raw """""").strip() ) ]]] [[[end]]]",27, 17,,"You can assign semantic column types to columns, which affect how values are rendered, validated, transformed, and edited. Built-in column types include url , email , json , and textarea . Plugins can register additional column types using the register_column_types plugin hook. Column types can optionally declare which SQLite column types they apply to using sqlite_types . Datasette will reject incompatible assignments. The built-in url , email , json , and textarea column types are all restricted to TEXT columns. The simplest form maps column names to type name strings: [[[cog config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: column_types: website: url contact: email extra_data: json notes: textarea """""").strip() ) ]]] [[[end]]] For column types that accept additional configuration, use an object with type and config keys: [[[cog config_example(cog, textwrap.dedent( """""" databases: mydatabase: tables: example_table: column_types: website: type: url config: prefix: ""https://"" """""").strip() ) ]]] [[[end]]]",27, 18,Performance and caching,"Datasette runs on top of SQLite, and SQLite has excellent performance. For small databases almost any query should return in just a few milliseconds, and larger databases (100s of MBs or even GBs of data) should perform extremely well provided your queries make sensible use of database indexes. That said, there are a number of tricks you can use to improve Datasette's performance.",27, 19,Immutable mode,"If you can be certain that a SQLite database file will not be changed by another process you can tell Datasette to open that file in immutable mode . Doing so will disable all locking and change detection, which can result in improved query performance. This also enables further optimizations relating to HTTP caching, described below. To open a file in immutable mode pass it to the datasette command using the -i option: datasette -i data.db When you open a file in immutable mode like this Datasette will also calculate and cache the row counts for each table in that database when it first starts up, further improving performance.",27, 20,"Using ""datasette inspect""","Counting the rows in a table can be a very expensive operation on larger databases. In immutable mode Datasette performs this count only once and caches the results, but this can still cause server startup time to increase by several seconds or more. If you know that a database is never going to change you can precalculate the table row counts once and store then in a JSON file, then use that file when you later start the server. To create a JSON file containing the calculated row counts for a database, use the following: datasette inspect data.db --inspect-file=counts.json Then later you can start Datasette against the counts.json file and use it to skip the row counting step and speed up server startup: datasette -i data.db --inspect-file=counts.json You need to use the -i immutable mode against the database file here or the counts from the JSON file will be ignored. You will rarely need to use this optimization in every-day use, but several of the datasette publish commands described in Publishing data use this optimization for better performance when deploying a database file to a hosting provider.",27, 21,HTTP caching,"If your database is immutable and guaranteed not to change, you can gain major performance improvements from Datasette by enabling HTTP caching. This can work at two different levels. First, it can tell browsers to cache the results of queries and serve future requests from the browser cache. More significantly, it allows you to run Datasette behind a caching proxy such as Varnish or use a cache provided by a hosted service such as Fastly or Cloudflare . This can provide incredible speed-ups since a query only needs to be executed by Datasette the first time it is accessed - all subsequent hits can then be served by the cache. Using a caching proxy in this way could enable a Datasette-backed visualization to serve thousands of hits a second while running Datasette itself on extremely inexpensive hosting. Datasette's integration with HTTP caches can be enabled using a combination of configuration options and query string arguments. The default_cache_ttl setting sets the default HTTP cache TTL for all Datasette pages. This is 5 seconds unless you change it - you can set it to 0 if you wish to disable HTTP caching entirely. You can also change the cache timeout on a per-request basis using the ?_ttl=10 query string parameter. This can be useful when you are working with the Datasette JSON API - you may decide that a specific query can be cached for a longer time, or maybe you need to set ?_ttl=0 for some requests for example if you are running a SQL order by random() query.",27, 22,datasette-hashed-urls,"If you open a database file in immutable mode using the -i option, you can be assured that the content of that database will not change for the lifetime of the Datasette server. The datasette-hashed-urls plugin implements an optimization where your database is served with part of the SHA-256 hash of the database contents baked into the URL. A database at /fixtures will instead be served at /fixtures-aa7318b , and a year-long cache expiry header will be returned with those pages. This will then be cached by both browsers and caching proxies such as Cloudflare or Fastly, providing a potentially significant performance boost. To install the plugin, run the following: datasette install datasette-hashed-urls Prior to Datasette 0.61 hashed URL mode was a core Datasette feature, enabled using the hash_urls setting. This implementation has now been removed in favor of the datasette-hashed-urls plugin. Prior to Datasette 0.28 hashed URL mode was the default behaviour for Datasette, since all database files were assumed to be immutable and unchanging. From 0.28 onwards the default has been to treat database files as mutable unless explicitly configured otherwise.",27, 23,Plugins,"Datasette's plugin system allows additional features to be implemented as Python code (or front-end JavaScript) which can be wrapped up in a separate Python package. The underlying mechanism uses pluggy . See the Datasette plugins directory for a list of existing plugins, or take a look at the datasette-plugin topic on GitHub. Things you can do with plugins include: Add visualizations to Datasette, for example datasette-cluster-map and datasette-vega . Make new custom SQL functions available for use within Datasette, for example datasette-haversine and datasette-jellyfish . Define custom output formats with custom extensions, for example datasette-atom and datasette-ics . Add template functions that can be called within your Jinja custom templates, for example datasette-render-markdown . Customize how database values are rendered in the Datasette interface, for example datasette-render-binary and datasette-pretty-json . Customize how Datasette's authentication and permissions systems work, for example datasette-auth-passwords and datasette-permissions-sql .",27, 24,Installing plugins,"If a plugin has been packaged for distribution using setuptools you can use the plugin by installing it alongside Datasette in the same virtual environment or Docker container. You can install plugins using the datasette install command: datasette install datasette-vega You can uninstall plugins with datasette uninstall : datasette uninstall datasette-vega You can upgrade plugins with datasette install --upgrade or datasette install -U : datasette install -U datasette-vega This command can also be used to upgrade Datasette itself to the latest released version: datasette install -U datasette You can install multiple plugins at once by listing them as lines in a requirements.txt file like this: datasette-vega datasette-cluster-map Then pass that file to datasette install -r : datasette install -r requirements.txt The install and uninstall commands are thin wrappers around pip install and pip uninstall , which ensure that they run pip in the same virtual environment as Datasette itself.",27, 25,One-off plugins using --plugins-dir,"You can also define one-off per-project plugins by saving them as plugin_name.py functions in a plugins/ folder and then passing that folder to datasette using the --plugins-dir option: datasette mydb.db --plugins-dir=plugins/",27, 26,Deploying plugins using datasette publish,"The datasette publish and datasette package commands both take an optional --install argument. You can use this one or more times to tell Datasette to pip install specific plugins as part of the process: datasette publish cloudrun mydb.db --install=datasette-vega You can use the name of a package on PyPI or any of the other valid arguments to pip install such as a URL to a .zip file: datasette publish cloudrun mydb.db \ --install=https://url-to-my-package.zip",27, 27,Controlling which plugins are loaded,"Datasette defaults to loading every plugin that is installed in the same virtual environment as Datasette itself. You can set the DATASETTE_LOAD_PLUGINS environment variable to a comma-separated list of plugin names to load a controlled subset of plugins instead. For example, to load just the datasette-vega and datasette-cluster-map plugins, set DATASETTE_LOAD_PLUGINS to datasette-vega,datasette-cluster-map : export DATASETTE_LOAD_PLUGINS='datasette-vega,datasette-cluster-map' datasette mydb.db Or: DATASETTE_LOAD_PLUGINS='datasette-vega,datasette-cluster-map' \ datasette mydb.db To disable the loading of all additional plugins, set DATASETTE_LOAD_PLUGINS to an empty string: export DATASETTE_LOAD_PLUGINS='' datasette mydb.db A quick way to test this setting is to use it with the datasette plugins command: DATASETTE_LOAD_PLUGINS='datasette-vega' datasette plugins This should output the following: [ { ""name"": ""datasette-vega"", ""static"": true, ""templates"": false, ""version"": ""0.6.2"", ""hooks"": [ ""extra_css_urls"", ""extra_js_urls"" ] } ]",27, 28,Seeing what plugins are installed,"You can see a list of installed plugins by navigating to the /-/plugins page of your Datasette instance - for example: https://fivethirtyeight.datasettes.com/-/plugins You can also use the datasette plugins command: datasette plugins Which outputs: [ { ""name"": ""datasette_json_html"", ""static"": false, ""templates"": false, ""version"": ""0.4.0"" } ] [[[cog from datasette import cli from click.testing import CliRunner import textwrap, json cog.out(""\n"") result = CliRunner().invoke(cli.cli, [""plugins"", ""--all""]) # cog.out() with text containing newlines was unindenting for some reason cog.outl(""If you run ``datasette plugins --all`` it will include default plugins that ship as part of Datasette:\n"") cog.outl("".. code-block:: json\n"") plugins = [p for p in json.loads(result.output) if p[""name""].startswith(""datasette."")] indented = textwrap.indent(json.dumps(plugins, indent=4), "" "") for line in indented.split(""\n""): cog.outl(line) cog.out(""\n\n"") ]]] If you run datasette plugins --all it will include default plugins that ship as part of Datasette: [ { ""name"": ""datasette.actor_auth_cookie"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""actor_from_request"" ] }, { ""name"": ""datasette.blob_renderer"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_output_renderer"" ] }, { ""name"": ""datasette.default_actions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_actions"" ] }, { ""name"": ""datasette.default_column_types"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_column_types"" ] }, { ""name"": ""datasette.default_database_actions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""database_actions"" ] }, { ""name"": ""datasette.default_debug_menu"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""jump_items_sql"" ] }, { ""name"": ""datasette.default_jump_items"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""jump_items_sql"" ] }, { ""name"": ""datasette.default_magic_parameters"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_magic_parameters"" ] }, { ""name"": ""datasette.default_permissions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""permission_resources_sql"" ] }, { ""name"": ""datasette.default_permissions.sqlite_statistics"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""permission_resources_sql"" ] }, { ""name"": ""datasette.default_permissions.tokens"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""actor_from_request"", ""register_token_handler"" ] }, { ""name"": ""datasette.default_query_actions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""query_actions"" ] }, { ""name"": ""datasette.default_table_actions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""table_actions"" ] }, { ""name"": ""datasette.events"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_events"", ""write_wrapper"" ] }, { ""name"": ""datasette.facets"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_facet_classes"" ] }, { ""name"": ""datasette.filters"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""filters_from_request"" ] }, { ""name"": ""datasette.forbidden"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""forbidden"" ] }, { ""name"": ""datasette.handle_exception"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""handle_exception"" ] }, { ""name"": ""datasette.publish.cloudrun"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""publish_subcommand"" ] }, { ""name"": ""datasette.publish.heroku"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""publish_subcommand"" ] }, { ""name"": ""datasette.sql_functions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""prepare_connection"" ] } ] [[[end]]] You can add the --plugins-dir= option to include any plugins found in that directory. Add --requirements to output a list of installed plugins that can then be installed in another Datasette instance using datasette install -r requirements.txt : datasette plugins --requirements The output will look something like this: datasette-codespaces==0.1.1 datasette-graphql==2.2 datasette-json-html==1.0.1 datasette-pretty-json==0.2.2 datasette-x-forwarded-host==0.1 To write that to a requirements.txt file, run this: datasette plugins --requirements > requirements.txt",27, 29,Plugin configuration,"Plugins can have their own configuration, embedded in a configuration file . Configuration options for plugins live within a ""plugins"" key in that file, which can be included at the root, database or table level. Here is an example of some plugin configuration for a specific table: [[[cog from metadata_doc import config_example config_example(cog, { ""databases"": { ""sf-trees"": { ""tables"": { ""Street_Tree_List"": { ""plugins"": { ""datasette-cluster-map"": { ""latitude_column"": ""lat"", ""longitude_column"": ""lng"" } } } } } } }) ]]] [[[end]]] This tells the datasette-cluster-map column which latitude and longitude columns should be used for a table called Street_Tree_List inside a database file called sf-trees.db .",27, 30,Secret configuration values,"Some plugins may need configuration that should stay secret - API keys for example. There are two ways in which you can store secret configuration values. The /-/config introspection endpoint redacts the values of any configuration keys whose names contain one of these substrings: secret , key , password , token , hash or dsn . Name your plugin's secret configuration keys accordingly - for example api_key or client_secret - so they are automatically redacted there. As environment variables . If your secret lives in an environment variable that is available to the Datasette process, you can indicate that the configuration value should be read from that environment variable like so: [[[cog config_example(cog, { ""plugins"": { ""datasette-auth-github"": { ""client_secret"": { ""$env"": ""GITHUB_CLIENT_SECRET"" } } } }) ]]] [[[end]]] As values in separate files . Your secrets can also live in files on disk. To specify a secret should be read from a file, provide the full file path like this: [[[cog config_example(cog, { ""plugins"": { ""datasette-auth-github"": { ""client_secret"": { ""$file"": ""/secrets/client-secret"" } } } }) ]]] [[[end]]] If you are publishing your data using the datasette publish family of commands, you can use the --plugin-secret option to set these secrets at publish time. For example, using Heroku you might run the following command: datasette publish heroku my_database.db \ --name my-heroku-app-demo \ --install=datasette-auth-github \ --plugin-secret datasette-auth-github client_id your_client_id \ --plugin-secret datasette-auth-github client_secret your_client_secret This will set the necessary environment variables and add the following to the deployed metadata.yaml : [[[cog config_example(cog, { ""plugins"": { ""datasette-auth-github"": { ""client_id"": { ""$env"": ""DATASETTE_AUTH_GITHUB_CLIENT_ID"" }, ""client_secret"": { ""$env"": ""DATASETTE_AUTH_GITHUB_CLIENT_SECRET"" } } } }) ]]] [[[end]]]",27, 31,Changelog,,27, 32,1.0a39 (2026-09-10),"This alpha release includes security fixes for permissions, SQL construction, HTML rendering, authentication and caching, plus improvements to application startup and write execution. See 0.65.4 for fixes that have been backported to the stable 0.65.x branch. The Datasette blog has more details on these releases . Some of the security fixes include: Table and view permission checks now take SQLite's case-insensitive names into account. See How permissions are resolved . Viewing a full-text search index table now checks you have permission to view the table from which it draws its content. Viewing SQLite statistics tables ( sqlite_stat1 through sqlite_stat4 ) is now denied by a default. Table schema display now obeys the view-table permission. Table filters using ?_through= require permission to view the intermediate table. Foreign-key target and suggestion APIs, incoming foreign-key relationships and their row counts now respect view-table permission. Row endpoints check permissions before resolving primary keys, to avoid revealing the existence of an otherwise invisible primary key. Improved permission checks for the create-table API. See The JSON write API . The write SQL interface now checks view-table permission for tables referenced by CREATE VIEW statements. Fixed SQL identifier escaping for column names from untrusted database schemas. Fixed HTML escaping for column names from untrusted database schemas. URL columns now render links only for validated HTTP or HTTPS URLs. Private and personalized dynamic responses now use Cache-Control: private, no-store . Anonymous dynamic responses vary by Cookie and Authorization . Actor cookies now respect expire_after . Restricted actors can no longer create API tokens. Stored-query create, edit and delete forms now block framing to prevent clickjacking. Configuration secret redaction now matches key names case-insensitively. SQLite extension loading is disabled after extensions supplied using --load-extension have been loaded.",27, 33,Other improvements and fixes,"db.execute_write() now has a default execution time limit of 2,000ms. Plugins can override this using time_limit_ms= or disable it using time_limit_ms=None . This limit is independent of the sql_time_limit_ms setting for read queries. Application startup now runs through ASGI lifespan events before requests are accepted, with a first-request fallback for hosts without lifespan support. Thanks, Alex Garcia . ( #2887 ) datasette serve now runs startup hooks and Uvicorn on the same event loop, preserving background tasks started by plugins. The minimum Uvicorn version is now 0.29. Thanks, Alex Garcia . ( #2886 ) Non-blocking writes using execute_write_fn(..., block=False) now return a distinct task UUID for every call and work correctly with num_sql_threads=0 . Thanks, Zain Dana Harper . ( #2860 , #2859 ) Dropping a table now disables its full-text search index first. ( #2874 ) Fixed CREATE VIEW SQL analysis on Python 3.10.",27, 34,1.0a38 (2026-08-06),"This release fixes a SQL injection security issue that affects Datasette instances that serve a mixture of public and private tables in the same database, with access configured using the Datasette permissions system . Site administrators who serve private tables in this way are advised to disable the execute-sql permission on that database to prevent users from accessing private tables using raw SQL queries. The bug that has been fixed would have allowed users with access to any public table to execute SQL injection attacks despite that restriction, giving them read-only access to data in private tables in the same database. This fix is also available in Datasette 0.65.3.",27, 35,1.0a37 (2026-07-14),"Performance improvement for SQL-backed permission checks, plus an improved permission debugging interface. SQL used to resolve permission checks now aggregates permission rules before joining them to resources, improving performance on instances with large schemas. ( #2832 ) The Permission check view permission debugger now explains why a decision was allowed or denied, including the matching rules. The interactive form can also test a hypothetical actor supplied as JSON, and the permissions documentation now describes resolution rules in more detail. ( #2841 ) db.execute_write(sql, ..., transaction=True) has a new transaction= parameter, which can be set to False for statements such as VACUUM that cannot run inside a transaction. Write tasks now start their transactions using BEGIN IMMEDIATE , which also ensures that writes are rolled back if the task fails. ( #2831 ) Refreshing a database's schema in Datasette's internal catalog is now performed as a single atomic operation. ( #2831 ) Fixed schema introspection, table pages, facets and table counts for tables with names containing a ] character. Thanks, TowyTowy . ( #2431 , #2846 ) /-/plugins.json once again returns a top-level JSON array of plugin objects, reverting the object envelope introduced in 1.0a36. This should fix a large number of trivial test failures in existing plugins. ( #2842 , #2843 )",27, 36,1.0a36 (2026-07-07),"The signature features of this alpha are new UIs for inserting multiple rows at once (from TSV, CSV or JSON) and for creating a table from rows , plus a large number of small JSON API consistency fixes in preparation for a 1.0 stable release. Table pages now offer an ""Insert multiple rows"" mode in the row insertion dialog. This accepts pasted TSV, CSV or JSON, previews the parsed rows before inserting them, validates unknown columns as data is pasted and displays omitted auto integer primary keys as auto in the preview. ( #2813 ) The bulk insert UI can skip rows with existing primary keys, or update existing rows and insert new rows using the existing ///-/upsert API when the actor has both insert-row and update-row permissions. ( #2813 ) The ""Create table"" dialog now includes a ""Create table from data"" mode. Paste TSV, CSV or JSON rows to preview inferred columns and types, choose the table name and primary key, then create the table and insert those rows in one step. ( #2813 ) Datasette's JSON APIs now consistently encode every BLOB value using the documented binary value JSON format , even when the bytes could be decoded as UTF-8 text. ( #2806 , #2822 ) The insert and edit row dialogs now provide a dedicated control for BLOB values. Existing binary values are shown by byte size, image values under 10MB are previewed as thumbnails, and replacements can be attached, dropped or pasted into the control. ( #2806 , #2822 ) The table and row JSON APIs now support ?_extra=column_details for returning SQLite schema details for columns, including declared type, SQLite affinity, primary key, NOT NULL , default and hidden-column metadata. POST bodies that Datasette reads fully into memory - such as JSON submitted to the write API - are now capped by the new max_post_body_bytes setting, defaulting to 2MB. Oversized requests are rejected with an HTTP 413 error as soon as the limit is exceeded, protecting smaller servers from memory exhaustion. File uploads are unaffected - request.form() streams those to disk and has its own separate limits. ( #2823 ) Row pages for tables with compound primary keys now return a 400 error instead of a 500 error when the URL row identifier does not contain the correct number of primary key values. Thanks, Zain Dana Harper . ( #2811 , #2815 ) The execute-write-sql interface now supports CREATE VIEW and DROP VIEW statements, gated by the new create-view and drop-view permissions. ( #2819 , #2818 ) Saved-query SQL analysis now handles recursive CTEs, fixing a bug where storing a valid read-only recursive query could be disabled by SQLite's internal SQLITE_RECURSIVE authorizer callback. ( #2809 , #2812 ) named_parameters() now correctly ignores SQLite comment markers that appear inside string literals, so query forms no longer drop later :named parameters from SQL such as select '--' || :name . Thanks, JSap0914 . ( #2783 ) Datasette's internal database schema is now managed using sqlite-utils migrations , using the new dependency on sqlite-utils>=4.0 . ( #2827 ) datasette.utils.CustomJSONEncoder is now documented as a public API for plugins that need to serialize Datasette values to JSON. Thanks, Chris Amico . ( #1983 , #1996 ) This release also includes the results of a detailed consistency review of Datasette's JSON API in preparation for the 1.0 stable release. Several of these changes are backwards-incompatible with previous 1.0 alphas. The new API stability documentation describes exactly which parts of the JSON API are covered by the 1.0 stability promise.",27, 37,JSON API: breaking changes,"JSON error responses now use a single canonical format across every endpoint: {""ok"": false, ""error"": ""..."", ""errors"": [...], ""status"": 400} . The error key joins all error messages together, errors is the full list of messages and status always matches the HTTP status code. The legacy title key is no longer included in JSON errors (it remains available to the HTML error template), and endpoints that previously returned bare {""error"": ...} objects have been updated. See Error responses . Every JSON object success response now includes ""ok"": true , including introspection endpoints such as /-/versions and /-/settings . /-/plugins.json , /-/databases.json and /-/actions.json now return objects - {""ok"": true, ""plugins"": [...]} and equivalents - instead of top-level JSON arrays, so these responses can gain additional keys in the future without a breaking change. The datasette plugins CLI command still outputs a plain array. /-/databases now only lists databases the current actor is allowed to view. It previously listed every attached database, including their filesystem paths, to any actor with view-instance . Requests with an invalid or expired Authorization: Bearer token now receive a 401 status with the standard error body and a WWW-Authenticate: Bearer error=""invalid_token"" header, instead of being silently treated as unauthenticated. Bearer tokens that no registered token handler recognizes are still ignored, so authentication plugins with their own token formats keep working. Plugin token handlers can raise the new datasette.TokenInvalid exception to trigger the same behavior. Permission errors for JSON requests now return the standard JSON error format with a 403 status. The default forbidden handling previously rendered an HTML error page even for .json requests. POST to a write canned query now returns a 400 error when the SQL fails to execute, instead of a 200 status with ""ok"": false in the body. The error response includes the standard error keys plus a ""redirect"" key. The row update API with ""return"": true now responds with a ""rows"" list, matching insert and upsert, instead of a singular ""row"" object. Row delete write failures - such as a constraint violation raised by a trigger - now return 400 instead of 500 , matching the other write endpoints. //-/query.json with a missing or blank ?sql= parameter now returns a 400 error, as the CSV format already did, instead of a 200 with empty rows. Unknown ?_extra= names now return a 400 error for JSON and other data formats, instead of being silently ignored. HTML pages continue to ignore unknown names. Table JSON responses now include next_url alongside next by default - both are null on the final page. The now-redundant ?_extra=next_url parameter has been removed. The stored query list JSON no longer includes has_more - ""next"": null is the end-of-results signal across the whole API. This change also uncovered and fixed a bug where the query list next_url pointed at the HTML page and was a relative path; it is now an absolute URL that preserves the requested format. Stored query JSON objects no longer duplicate the list of parameter names as both params and parameters - only parameters remains. The query create and update APIs no longer accept params as an input alias either; params is still the documented key for queries defined in configuration . Page size parameters are now consistent across the API: the stored query lists accept ?_size=max and return a 400 error for values over the maximum instead of silently clamping them, and the /-/allowed and /-/rules permission debug endpoints renamed their page and page_size parameters to _page and _size , matching the underscore grammar used by every other Datasette system parameter. /-/threads now requires the permissions-debug permission, since it exposes runtime internals such as file paths. It previously only required view-instance . Trusted stored queries - those defined in configuration - can no longer be deleted through the JSON API or web interface, matching the existing restriction on editing them. The //-/schema endpoints now check the view-database permission before checking whether the database exists, so unauthorized actors can no longer probe for the existence of databases. SQL time limit errors in JSON responses are now a plain text message. The error string previously embedded an HTML fragment. The undocumented homepage JSON at /.json now returns databases as a list of objects rather than an object keyed by database name, matching every other collection in the API. The legacy .jsono format extension, long since superseded by ?_shape= , has been removed.",27, 38,JSON API: other improvements,"The write API endpoints now parse the request body as JSON regardless of the Content-Type header, so curl -d invocations work without remembering to set it. Invalid JSON is a 400 error. Cross-site request forgery remains prevented by Datasette's Origin and Sec-Fetch-Site checks. This also fixes a 500 error from the insert API when the Content-Type header was missing entirely. New Response.error(messages, status=400) helper for plugins that need to return a JSON error in Datasette's standard format. See Response class . New count_truncated extra for table JSON, included automatically whenever count is requested. true means the count reached Datasette's counting limit and the real number of rows may be higher. See Expanding JSON responses . JSON endpoints that are not part of the documented stable API now declare themselves with an ""unstable"" key in their responses. New documentation covering the grammar for boolean query string arguments , the reason upsert returns 200 where insert returns 201 , and advice for plugin authors on naming secret configuration keys so that /-/config redacts them automatically.",27, 39,1.0a35 (2026-06-23),"This release adds UI for creating tables and altering tables , to complement the insert and update row interfaces added in 1.0a34 (2026-06-16) . New ""Create table"" interface in the database actions menu, backed by the //-/create JSON API . It can define columns, primary keys, custom column types, NOT NULL constraints, literal defaults, expression defaults and single-column foreign keys. ( #2787 ) New ""Alter table"" table action and //
/-/alter JSON API for changing existing tables: add, rename, reorder and drop columns; change column types, defaults, NOT NULL constraints, primary keys and foreign keys; and rename the table. The alter table dialog also includes a ""Drop table"" button. ( #2788 ) New //-/foreign-key-targets and //
/-/foreign-key-suggestions JSON APIs for discovering valid single-column foreign key targets and suggested relationships. New Template context documentation listing the variables available to custom templates for Datasette's core pages. Variables documented there are treated as a stable API for custom templates until Datasette 2.0. The documentation is generated from dataclass definitions next to the view code, with tests that compare the documented fields against the actual contexts rendered by the database, table, query and row pages. ( #1510 , #2127 , #1477 , #2803 ) The ""Write to this database"" page now includes a Create table starter template, alongside the existing Insert, Update and Delete templates. ( #2794 ) New static() template function and datasette.static() method for generating cache-busting static asset URLs based on the file contents. Static assets served with a matching ?_hash= parameter now receive far-future immutable cache headers. This works for Datasette's bundled static assets, plugin static assets and directories mounted using --static . See Linking to static assets . Database and table pages now use the count_truncated template context value to display capped row counts as >N rows . Significant visual improvements to the table filter form UI, plus working add/remove filter buttons. ( #2798 ) Improved edit row icon on table pages. ( #2796 ) Documentation covers how actors are displayed. Thanks, Sebastian Cao . ( #2002 ) Fix for bug where appending ?_col=pk resulted in duplicate primary key columns in the response. Thanks, Ritesh Kewlani . ( #1975 )",27, 40,1.0a34 (2026-06-16),"The big feature in this alpha is tools to insert, edit and delete rows within the Datasette interface. These features are available on table pages, and edit and delete are also available as action items on the row page. The edit interface takes custom column types into account. Plugins that define their own column types can use JavaScript to customize how those column types are presented in the edit interface. datasette.allowed_many() method for resolving multiple permission checks at once . ( #2775 ) Permission checks are now cached on a per-request basis, speeding up table pages with multiple plugins that check permissions in order to populate the table actions menu . Fixed a warning about gen.throw(*sys.exc_info()) . ( #2776 ) New default custom column type textarea for multi-line text content. This is rendered as a