Viewing a local database is trivial — any desktop client will do. Viewing a production one safely is a different question, and the answer is never to expose the database port to the internet so a viewer can reach it. Tunnel through SSH, connect with a read-only role, or use a browser-based viewer that lives inside the platform.
Search for database viewer and the results are overwhelmingly desktop tools for local SQLite files. Useful, and not the situation most people are actually in. The real question is usually how to look at the live database that is behind an application right now, without breaking it and without leaving a hole behind.
Table of contents
- What people actually need a viewer for
- Never expose the database port
- The SSH tunnel, which is the standard answer
- Connect as a read-only user
- Browser-based viewers, and the phpMyAdmin question
- Practices worth adopting whichever tool you use
- How this fits the rest of the stack
- FAQ
What people actually need a viewer for
Naming the tasks makes the tooling choice obvious, because they have quite different risk profiles.
- Checking a value. Did that setting save? Is this user’s flag actually set? Read-only, five seconds, low risk.
- Investigating a bug. Reading several related rows to work out how the data got into a state the code did not expect. Read-only, exploratory.
- Fixing one row. A stuck order, a typo in a configuration row. A single write, high risk, needs care.
- Exporting. Pulling a table out as CSV for analysis or for a colleague.
- Importing. Restoring a table or loading a fixture. High risk.
- Understanding the schema. What columns exist, what the indexes are, how the tables relate.
Four of those six are read-only, and they represent the overwhelming majority of the times anyone opens a database viewer. That matters because it means most access does not need write permission, and granting write permission by default is a choice rather than a necessity.
Never expose the database port
The tempting shortcut when a desktop client cannot reach a remote database is to open the port — 3306 for MySQL, 5432 for Postgres — to your IP address, or worse, to everything.
Do not. A database port reachable from the internet is scanned within minutes of being opened. Automated credential-stuffing against database ports is constant background traffic on the internet, and the outcome ranges from a ransom note in a table to a quiet copy of your data taken over a weekend.
Restricting to your own IP address is better and still poor. Home addresses change, office VPNs egress from ranges you do not fully control, and the rule invariably outlives the reason it was added — somebody opens it for an afternoon of debugging and it is still there two years later.
The general principle: the database should be reachable only from the application that uses it, on a private network. Anything else needs a deliberate, temporary, audited path.
Private networking is the standard arrangement for managed databases and is how RunxBuild’s MySQL and Postgres instances are set up — the service talks to the database over the private network rather than the database having a public address at all.
The SSH tunnel, which is the standard answer
An SSH tunnel gives your local client a connection to the remote database without the database being publicly reachable. Traffic goes over SSH, which is already exposed and already authenticated with keys.
# Forward local port 5433 to the database, via a host that can reach it
ssh -N -L 5433:db-internal-host:5432 [email protected]
# Then point your client at localhost:5433
psql -h 127.0.0.1 -p 5433 -U readonly_user mydb
Use a local port that differs from the standard one — 5433 rather than 5432 — so you cannot accidentally connect to your local development database while believing you are on production, or the reverse. That mistake has caused real damage and the different port number prevents it entirely.
Every reasonable desktop client supports this natively: DBeaver, TablePlus, DataGrip, and pgAdmin all have an SSH tunnel tab where you provide the jump host and key, and they manage the forwarding for you.
The tunnel closes when you close it. Nothing is left open, there is no firewall rule to forget, and the access is tied to your SSH key, which can be revoked with everyone else’s when someone leaves.
Connect as a read-only user
The second half of doing this safely, and the half that gets skipped because the application’s credentials are the ones lying around.
Connecting to production with the application’s account means one mistyped UPDATE without a WHERE clause is a data-loss incident. A read-only role makes that impossible rather than unlikely.
-- PostgreSQL
CREATE ROLE analyst LOGIN PASSWORD 'strong-password-here';
GRANT CONNECT ON DATABASE mydb TO analyst;
GRANT USAGE ON SCHEMA public TO analyst;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analyst;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO analyst;
-- MySQL
CREATE USER 'analyst'@'%' IDENTIFIED BY 'strong-password-here';
GRANT SELECT ON mydb.* TO 'analyst'@'%';
FLUSH PRIVILEGES;
The ALTER DEFAULT PRIVILEGES line in the Postgres version is the one people forget. Without it, tables created after you granted access are not covered, and the role starts failing on new tables in a way that looks like a permissions bug.
Keep write access for the rare occasion it is genuinely needed, as a separate credential that is deliberately awkward to use. The friction is the point.
Even read-only access is not risk-free on a busy database — an unqualified SELECT against a large table can consume significant resources. Set a statement timeout on the role so an exploratory query cannot run for twenty minutes.
Browser-based viewers, and the phpMyAdmin question
phpMyAdmin and Adminer are the traditional web-based answer, and they have a specific problem: they are a web application with database credentials, deployed on the same server as the site, at a predictable URL.
That combination is heavily scanned. A phpMyAdmin installation at the obvious path with a weak password is one of the more reliable ways to lose a database. If you run one, it needs to be behind authentication at the web-server level, restricted by IP, kept updated, and ideally not at the default location.
A database browser built into the hosting dashboard avoids the whole category. There is no separate application to update, no second set of credentials, no public URL, and access is governed by who is on the team rather than by who knows a password.
This is how RunxBuild’s managed WordPress works — the dashboard includes a database browser covering tables, rows, SQL, export, and import, alongside a file manager. For the tasks in the first section, which are mostly reading a value or exporting a table, that removes both the SFTP credentials and the phpMyAdmin installation.
The general lesson holds regardless of platform: a viewer that is part of an authenticated control plane you already log into is safer than a viewer that is a separate publicly reachable application with its own credentials.
Practices worth adopting whichever tool you use
- Read-only by default. Write access should be a separate, deliberate step.
- Set a statement timeout so an exploratory query cannot become an incident.
- Never open the database port to the internet, not even temporarily, not even to one IP.
- Use a non-standard local port for tunnels so you cannot confuse environments.
- Colour-code connections in your client. Every good desktop client supports this and a red production tab has prevented a great many mistakes.
- Before any manual write, wrap it in a transaction, run the SELECT version of the WHERE clause first, confirm the row count, then commit.
- Take an export before a manual change to anything that matters.
- Prefer a script in the repository over a manual edit for anything you might need to do twice or explain later.
The transaction habit deserves emphasis because it costs nothing:
BEGIN;
SELECT count(*) FROM orders WHERE status = 'pending' AND created_at < '2026-01-01';
-- confirm the number is what you expected
UPDATE orders SET status = 'expired' WHERE status = 'pending' AND created_at < '2026-01-01';
-- confirm the affected row count matches
COMMIT;
Two extra keystrokes and a moment of attention, and the failure mode changes from irreversible to a rollback.
How this fits the rest of the stack
Most database viewing is reading, which means most access should be read-only, tunnelled, and time-limited rather than a port opened for convenience. The safest viewer is one already inside a control plane you authenticate to, which is why a database browser in the dashboard beats a separately deployed web tool with its own credentials. Managed MySQL and Postgres on RunxBuild sit on the private network with user management and connection limits handled at the platform level, and the RunxBuild hosting calculator shows the database next to the service using it.
Useful related references:
- Redis vs DynamoDB: Cache, Database, or Both
- MySQL to MySQL: Migrating a Database Between Servers
- What Is a Query in a Database? The Answer That Actually Helps You Write One
- Databases on RunxBuild
FAQ
How do I view a remote database without exposing it?
Use an SSH tunnel. Forward a local port through a host that can already reach the database, then point your desktop client at localhost. Every serious client — DBeaver, TablePlus, DataGrip, pgAdmin — supports this natively. The database stays on the private network and nothing is left open when you disconnect.
Is it safe to open port 3306 or 5432 to my IP address?
Better than opening it to everyone, and still not advisable. Home and office addresses change, the rule outlives the reason it was added, and exposed database ports are scanned constantly. A tunnel achieves the same access with no persistent firewall change and access tied to a revocable SSH key.
Should I use phpMyAdmin on production?
Only with real care. It is a publicly reachable web application holding database credentials at a predictable URL, which makes it a heavily scanned target. If you run one, put it behind web-server authentication, restrict by IP, keep it updated, and move it off the default path. A database browser inside your hosting dashboard avoids the category entirely.
How do I create a read-only database user?
In Postgres, grant CONNECT, USAGE on the schema, and SELECT on all tables, then add ALTER DEFAULT PRIVILEGES so tables created later are covered too — that last part is the step most often missed. In MySQL, GRANT SELECT on the database. Add a statement timeout so an exploratory query cannot run away.
What is the safest way to make a manual change to production data?
Wrap it in a transaction. Run the SELECT form of your WHERE clause first and confirm the row count, then run the UPDATE, confirm the affected rows match, and only then commit. Take an export beforehand. If you might need to do it twice, write it as a migration in the repository instead.