Browse SQLite, with a read-only mode that holds
SQLite is a file, which makes it the easiest database to share and the easiest to change by accident. Opening it read-only is a property of how it is opened rather than of a user account, and that distinction is the whole of the safety story here.
Create a read-only SQLite user
Verified: opened with mode=ro, SELECT returned rows while INSERT and DROP both raised. Note that mode=ro still requires the process to be able to READ the file, and a plain path without the file: URI silently opens read-write.
-- SQLite has no users and no GRANT. Read-only is the OPEN MODE:
-- file:/path/to/app.db?mode=ro
-- Python, verified
import sqlite3
conn = sqlite3.connect("file:/path/to/app.db?mode=ro", uri=True)
-- Belt and braces: make the file itself unwritable for the process
chmod 444 /path/to/app.dbWhat browsing SQLite looks like
- Declared types are shown alongside what the values actually are, which matters because SQLite does not enforce the declaration.
- Dates arrive as text, integers or floats depending on what wrote them, and the console shows which.
- Foreign keys are displayed where declared — whether they were ever enforced is a separate question.
- The whole database is one file, so what you are looking at is unambiguous.
What catches people out on SQLite
The declared type is advisory
A column declared INTEGER will happily hold the string 'banana', because SQLite uses type affinity rather than enforcement. If a column looks wrong, the data may genuinely be wrong rather than misrendered.
Foreign keys are off unless switched on per connection
Every connection must issue PRAGMA foreign_keys = ON. A schema full of relationships may contain orphans that nothing ever rejected.
There is no date type
Dates are text, Unix integers or Julian floats, decided by whatever wrote them. Sorting and comparing only behave if the format sorts lexicographically, which is why ISO-8601 is the one to use.
A read-only login still shows everything
The user above stops writes. It does not stop someone reading every email address, card number and salary in the database, and it does not record who did. The console adds the other half: sensitive columns are detected and masked before the rows leave the server, unmasking is granted per column to named people, and every access is logged. Access Review answers "who can see what" across every source in one report, which is the question that is very hard to answer from a pile of grants.
Questions
How do I open a SQLite database read-only?
Open it through a file: URI with mode=ro. Verified: SELECT worked while INSERT and DROP were refused. A plain filesystem path opens read-write, so the URI form is the part that matters. Setting the file permissions to read-only is a useful second layer.
Why does a column contain the wrong type?
Because SQLite does not enforce declared types. The declaration is an affinity, and any value can be stored in any column, so what you are seeing is probably what is really there.
Other databases
PostgreSQL
Browse a PostgreSQL database with PII masked, follow foreign keys, and give your team read-only access without sharing the connection string.
MySQL
Browse a MySQL database from the browser with PII masked, follow foreign keys, and give teammates read-only access without sharing the root password.
MariaDB
Browse MariaDB in the browser with PII masked and per-person access, without handing out a shared database login.
SQL Server
Browse SQL Server from the browser with PII masked and per-person access, without distributing a login or installing SSMS.
Oracle
Browse an Oracle database in the browser with PII masked and per-person access, without granting a schema account to everyone who needs a look.
MongoDB
Browse MongoDB collections in the browser with PII masked — and the one configuration detail that decides whether a read-only user means anything at all.