<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Varun Krishnan</title>
    <description>The latest articles on DEV Community by Varun Krishnan (@not_varunkv).</description>
    <link>https://dev.to/not_varunkv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3608714%2F3d7fbcf6-347c-4905-b0ec-2b7ec91677b4.jpg</url>
      <title>DEV Community: Varun Krishnan</title>
      <link>https://dev.to/not_varunkv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/not_varunkv"/>
    <language>en</language>
    <item>
      <title>SQL to Schema Diagram Online: Convert SQL to ERD in Seconds</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Thu, 01 Oct 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/sql-to-schema-diagram-online-convert-sql-to-erd-in-seconds-fhk</link>
      <guid>https://dev.to/not_varunkv/sql-to-schema-diagram-online-convert-sql-to-erd-in-seconds-fhk</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You have SQL. You need a diagram. The fastest path from `CREATE TABLE` to a visual schema is a tool that parses SQL and renders an ER diagram on the spot.

[dbdiagramr](https://www.dbdiagramr.space/visualize) does exactly that -- paste your SQL, get an interactive diagram in seconds. No signup, no install, no export limits.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Why convert SQL to a schema diagram?
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQL is precise. Diagrams are comprehensible. When you're onboarding to a new codebase or explaining a schema to a non-technical stakeholder, a visual representation does something SQL can't: it shows relationships at a glance.

Reading 200 lines of `CREATE TABLE` statements, you'll find the foreign keys eventually. Seeing them drawn as lines between boxes takes half a second.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  How to convert SQL to a schema diagram online
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Get your SQL
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Grab your schema from one of these sources:


  - `pg_dump --schema-only yourdb`
  - Migration files (Prisma, Drizzle, Rails, Django)
  - Your IDE's schema export
  - A SQL file you already have
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Step 2: Paste into dbdiagramr
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Go to [dbdiagramr.com/visualize](https://www.dbdiagramr.space/visualize) and paste your SQL in the editor. The tool parses CREATE TABLE statements, detects primary keys, and maps foreign key relationships automatically.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Step 3: Explore and export
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your ER diagram appears instantly. Pan, zoom, drag tables around to arrange them. When you're happy, export as SVG or PNG with one click.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  What SQL syntax works?
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dbdiagramr understands standard PostgreSQL syntax:


  - `CREATE TABLE` with columns and types
  - `PRIMARY KEY` constraints
  - `REFERENCES` for foreign keys
  - `DEFAULT` values
  - `NOT NULL` and `UNIQUE` constraints
  - `ON DELETE` and `ON UPDATE` actions

If your SQL is valid PostgreSQL, dbdiagramr will parse it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Example: converting a Laravel migration
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If you're using Laravel, your migrations live in `database/migrations/`. Export them to SQL and paste the result:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Laravel migration exported to SQL&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;remember_token&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dbdiagramr renders the `users` and `posts` tables with the foreign key relationship drawn between them. Done in seconds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Alternative methods (and why they're slower)
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    | pgAdmin ERD tool | Already installedMediumSnapshot, manual re-run |
    | Draw.io + manual | ShortSlowHand-draw every table |
    | dbdiagram.io | Account requiredFastMust learn DBML syntax |
    | dbdiagramr | NoneInstantPostgreSQL only (for now) |


dbdiagramr is the only option that requires zero setup and accepts raw SQL directly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**Can I convert MySQL SQL to a diagram?**Not yet. dbdiagramr currently supports PostgreSQL syntax only. MySQL support is planned.

**Does it work with Prisma schema files?**Not directly. Export your Prisma schema to SQL first with `prisma db pull` or `pg_dump`, then paste the SQL.

**How large a schema can it handle?**dbdiagramr handles schemas with dozens of tables comfortably. Very large schemas (100+ tables) may need some manual arrangement, but the parsing works fine.

**Is there a table limit?**No. Paste as many tables as you want. No signup, no limits.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Free Online Database Diagram Tool: Visualize Your Schema in Seconds</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/free-online-database-diagram-tool-visualize-your-schema-in-seconds-4al7</link>
      <guid>https://dev.to/not_varunkv/free-online-database-diagram-tool-visualize-your-schema-in-seconds-4al7</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You need a database diagram. You don't want to pay for it, sign up for another account, or install anything. A free online database diagram tool should let you paste SQL or connect to your database and get a visual schema in seconds.

Most tools claim to be free, then hit you with table limits, export restrictions, or require an email before you can do anything. The one that actually works the way you expect is [dbdiagramr](https://www.dbdiagramr.space) -- paste SQL or a PostgreSQL connection string, get an interactive ER diagram, export SVG/PNG. No signup, no limits, no catch.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  What a good free database diagram tool should do
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    | Paste SQL directly | You already have the schema -- just show it |
    | Live database connection | See what's actually deployed, not what you think is deployed |
    | Interactive diagram | Pan, zoom, drag tables around |
    | Export SVG/PNG | Share with your team or drop in docs |
    | Search tables/columns | Find anything in a large schema fast |
    | No signup required | You're diagramming, not buying enterprise software |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  How to use dbdiagramr as your free database diagram tool
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Option 1: Paste SQL
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If you have a SQL dump or migration file, paste it directly into dbdiagramr and see your ER diagram appear instantly. Works with PostgreSQL CREATE TABLE statements, foreign keys, indexes -- the whole schema.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Option 2: Connect to a live database
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If you have a PostgreSQL connection string, paste it into dbdiagramr. It queries `information_schema` for tables, columns, primary keys, and foreign keys, then renders an interactive ER diagram you can pan, zoom, drag, and export as SVG or PNG. Your SQL never leaves your browser -- the connection happens client-side.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Option 3: Upload a file
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Drag and drop a `.sql` file or use the file picker. Same result -- instant diagram.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Why most "free" database diagram tools aren't actually free
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    | dbdiagram.io | 10 tables on free plan, export requires paid plan |
    | DrawSQL | Limited to 10 diagrams, no collaboration on free plan |
    | Lucidchart | 3 editable documents, 60 shapes per document |
    | QuickDBD | Limited diagrams, watermark on exports |


dbdiagramr has none of these limits. Paste SQL, connect to your database, export as much as you want. The tool is open-source -- you can even self-host it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Real example: visualizing a Supabase schema
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Supabase projects come with auth, storage, and realtime tables out of the box. Understanding how they connect requires a diagram.

Paste this SQL into dbdiagramr:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;post_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;In two seconds you see: `users` -- `posts` -- `comments` with all foreign key relationships drawn. Drag the boxes around, zoom in, export as PNG for your README.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  When to use a free tool vs. a paid one
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**Use a free tool when:**


  - You're exploring an existing schema
  - You need a quick diagram for documentation
  - You're a solo developer or small team
  - You want to understand relationships before writing queries

**Consider paid when:**


  - You need real-time collaboration (Lucidchart, DrawSQL paid)
  - You need version control integration (some enterprise tools)
  - You're designing a schema from scratch with a large team

For 90% of database diagramming tasks, a free tool is enough.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**What's the best free online database diagram tool?**dbdiagramr is the best free option -- no table limits, no signup, open-source. Paste SQL or connect to a live PostgreSQL database.

**Can I create a database diagram without signing up?**Yes. dbdiagramr requires no account. Go to the site, paste SQL, get your diagram.

**Does it work with PostgreSQL only?**Currently yes. PostgreSQL is the most common target for ER diagrams, and dbdiagramr specializes in it. SQL paste works with any PostgreSQL-compatible syntax.

**Can I export the diagram?**Yes. Export as SVG or PNG with one click. No paid plan required.

**Is my database connection secure?**Yes. dbdiagramr connects to your database client-side. Your connection string never leaves your browser. No data is sent to any server.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>dbdiagram.io vs dbdiagramr vs DrawSQL: Honest Comparison</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/dbdiagramio-vs-dbdiagramr-vs-drawsql-honest-comparison-4794</link>
      <guid>https://dev.to/not_varunkv/dbdiagramio-vs-dbdiagramr-vs-drawsql-honest-comparison-4794</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;dbdiagram.io is the original -- great for writing schemas in code (DBML), but no live database connection. DrawSQL is a paid GUI tool with good auto-generation but requires a desktop app. dbdiagramr is free, runs in the browser, connects directly to Supabase/Neon/Railway, and generates diagrams from your live database in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;dbdiagram.io&lt;/th&gt;
&lt;th&gt;dbdiagramr&lt;/th&gt;
&lt;th&gt;DrawSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free (1 diagram), $9/mo for more&lt;/td&gt;
&lt;td&gt;Free (unlimited)&lt;/td&gt;
&lt;td&gt;$8/mo (individual), $15/mo (team)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Live database connection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Supabase, Neon, Railway, any Postgres)&lt;/td&gt;
&lt;td&gt;Yes (MySQL, Postgres, SQLite)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBML support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (native)&lt;/td&gt;
&lt;td&gt;No (generates from DB)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto-generate from DB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (paste connection string)&lt;/td&gt;
&lt;td&gt;Yes (desktop app)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Supabase integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (direct connection)&lt;/td&gt;
&lt;td&gt;No (generic Postgres)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Neon integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Browser-based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (desktop app)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Schema export&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DBML, SQL&lt;/td&gt;
&lt;td&gt;SQL, PNG&lt;/td&gt;
&lt;td&gt;SQL, PNG, PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Collaboration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (paid)&lt;/td&gt;
&lt;td&gt;No (yet)&lt;/td&gt;
&lt;td&gt;Yes (paid)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (paid)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI context (llms.txt)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  dbdiagram.io
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams that want to design schemas in code (DBML) before creating them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DBML is a great way to define schemas as code.&lt;/li&gt;
&lt;li&gt;Clean, fast editor.&lt;/li&gt;
&lt;li&gt;Good for greenfield projects where you're designing from scratch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No live database connection -- you can't diagram an existing database.&lt;/li&gt;
&lt;li&gt;Free tier is limited to 1 diagram.&lt;/li&gt;
&lt;li&gt;Can't generate from Supabase, Neon, or Railway directly.&lt;/li&gt;
&lt;li&gt;Requires learning DBML syntax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (1 diagram), $9/mo (10 diagrams), $17/mo (unlimited).&lt;/p&gt;

&lt;h2&gt;
  
  
  dbdiagramr
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Developers who want to see their existing database schema in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free and unlimited.&lt;/li&gt;
&lt;li&gt;Paste a connection string, get a diagram -- no signup, no install.&lt;/li&gt;
&lt;li&gt;Direct Supabase, Neon, Railway integration.&lt;/li&gt;
&lt;li&gt;Browser-based, works on any device.&lt;/li&gt;
&lt;li&gt;Generates AI context (llms.txt) for your schema.&lt;/li&gt;
&lt;li&gt;Open source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only (can't edit schemas in the UI, yet).&lt;/li&gt;
&lt;li&gt;No collaboration features (yet).&lt;/li&gt;
&lt;li&gt;No DBML support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free.&lt;/p&gt;

&lt;h2&gt;
  
  
  DrawSQL
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams that want a desktop GUI for database exploration and documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Good auto-generation from live databases.&lt;/li&gt;
&lt;li&gt;Clean UI with export options (PNG, PDF, SQL).&lt;/li&gt;
&lt;li&gt;Supports MySQL, Postgres, SQLite.&lt;/li&gt;
&lt;li&gt;Collaboration features for teams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop app -- not browser-based.&lt;/li&gt;
&lt;li&gt;Paid (no free tier for most features).&lt;/li&gt;
&lt;li&gt;No direct Supabase integration (generic Postgres only).&lt;/li&gt;
&lt;li&gt;No AI context generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: $8/mo (individual), $15/mo (team), $25/mo (organization).&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you...&lt;/th&gt;
&lt;th&gt;Use...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Want to diagram an existing Supabase/Neon database&lt;/td&gt;
&lt;td&gt;dbdiagramr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want to design a schema from scratch in code&lt;/td&gt;
&lt;td&gt;dbdiagram.io&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need a desktop GUI with export options&lt;/td&gt;
&lt;td&gt;DrawSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want free, unlimited diagrams&lt;/td&gt;
&lt;td&gt;dbdiagramr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need team collaboration&lt;/td&gt;
&lt;td&gt;dbdiagram.io or DrawSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want AI context for your schema&lt;/td&gt;
&lt;td&gt;dbdiagramr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are on a tight budget&lt;/td&gt;
&lt;td&gt;dbdiagramr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The real question
&lt;/h2&gt;

&lt;p&gt;The tools aren't competitors -- they serve different workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design first&lt;/strong&gt; (dbdiagram.io): You write DBML, generate migrations, deploy to production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing database&lt;/strong&gt; (dbdiagramr): You already have a database, you want to see what it looks like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GUI exploration&lt;/strong&gt; (DrawSQL): You want a desktop app to browse, query, and export your schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most developers use dbdiagramr to understand what they have, then dbdiagram.io if they want to redesign it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I import a dbdiagram.io diagram into dbdiagramr?
&lt;/h3&gt;

&lt;p&gt;Not directly. dbdiagram.io uses DBML format. dbdiagramr connects to live databases. You'd need to deploy your DBML schema to a database first, then connect dbdiagramr to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does dbdiagramr support MySQL?
&lt;/h3&gt;

&lt;p&gt;Not yet. Currently supports PostgreSQL only. MySQL support is planned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is DrawSQL worth the money?
&lt;/h3&gt;

&lt;p&gt;If you need a desktop GUI with export options and team collaboration, yes. If you just need to see your schema, dbdiagramr does it for free in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use dbdiagramr with Prisma?
&lt;/h3&gt;

&lt;p&gt;Yes. Prisma connects to PostgreSQL. Use your Prisma connection string (with &lt;code&gt;?pgbouncer=true&lt;/code&gt; if using Supabase pooler) and dbdiagramr will diagram your entire schema.&lt;/p&gt;

</description>
      <category>database</category>
      <category>tutorial</category>
      <category>postgres</category>
      <category>tools</category>
    </item>
    <item>
      <title>Supabase Connection String: IPv6, ENOTFOUND, and the Transaction Pooler Fix</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Thu, 24 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/supabase-connection-string-ipv6-enotfound-and-the-transaction-pooler-fix-17pk</link>
      <guid>https://dev.to/not_varunkv/supabase-connection-string-ipv6-enotfound-and-the-transaction-pooler-fix-17pk</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Supabase uses IPv6 by default. Most local dev environments and many hosting providers don't support IPv6, so DNS resolution fails with &lt;code&gt;ENOTFOUND&lt;/code&gt; or hangs with &lt;code&gt;ETIMEDOUT&lt;/code&gt;. The fix: use the transaction pooler connection string (port &lt;code&gt;6543&lt;/code&gt;) instead of the direct connection (port &lt;code&gt;5432&lt;/code&gt;). It routes through Supabase's IPv4-compatible proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error
&lt;/h2&gt;

&lt;p&gt;You're running your app locally and you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: getaddrinfo ENOTFOUND db.xxxxxxxxx.supabase.co
    at GetAddrInfoReq.onlookup [as oncomplete] (node:dns:1107:26)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: connect ETIMEDOUT 2600:1ff:f000:e000::1:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one means DNS can't resolve the hostname. The second means DNS resolved it to an IPv6 address, but the connection timed out because your network doesn't route IPv6.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Supabase assigns each project a hostname like &lt;code&gt;db.xxxxxxxxx.supabase.co&lt;/code&gt;. This hostname resolves to an IPv6 address by default. If your machine or hosting provider doesn't support IPv6, the connection fails.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;IPv6 support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS (most setups)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux (most setups)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows (most setups)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Docker&lt;/td&gt;
&lt;td&gt;Depends on config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The three connection strings
&lt;/h2&gt;

&lt;p&gt;Supabase gives you three connection strings in the dashboard (Settings → Database):&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Direct connection (port 5432)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="n"&gt;YOUR&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xxxxxxxxx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;co&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Full Postgres protocol support&lt;/li&gt;
&lt;li&gt;Supports prepared statements, &lt;code&gt;SET&lt;/code&gt; commands, &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Uses IPv6 -- may fail locally or on some hosts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Transaction pooler (port 6543)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;YOUR&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;PROJECT&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;REF&lt;/span&gt;&lt;span class="p"&gt;]:[&lt;/span&gt;&lt;span class="n"&gt;YOUR&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;pooler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6543&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Routes through Supabase's connection pooler (PgBouncer)&lt;/li&gt;
&lt;li&gt;IPv4-compatible -- works everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does NOT support&lt;/strong&gt; prepared statements or &lt;code&gt;SET&lt;/code&gt; commands&lt;/li&gt;
&lt;li&gt;Best for most web apps&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Session pooler (port 6543, mode=session)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;YOUR&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;PROJECT&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;REF&lt;/span&gt;&lt;span class="p"&gt;]:[&lt;/span&gt;&lt;span class="n"&gt;YOUR&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;pooler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6543&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="n"&gt;pgbouncer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;session_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Same as transaction pooler but preserves session state&lt;/li&gt;
&lt;li&gt;Supports &lt;code&gt;SET&lt;/code&gt; commands but not prepared statements&lt;/li&gt;
&lt;li&gt;Use this if you need &lt;code&gt;SET search_path&lt;/code&gt; or similar&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which one to use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Connection string&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local dev (Node.js, Python, etc.)&lt;/td&gt;
&lt;td&gt;Transaction pooler (6543)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel / Railway / Render&lt;/td&gt;
&lt;td&gt;Transaction pooler (6543)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;Transaction pooler (6543)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma ORM&lt;/td&gt;
&lt;td&gt;Transaction pooler (6543)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drizzle ORM&lt;/td&gt;
&lt;td&gt;Transaction pooler (6543)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;Session pooler (6543 + session_mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laravel&lt;/td&gt;
&lt;td&gt;Transaction pooler (6543)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pg_dump / pg_restore&lt;/td&gt;
&lt;td&gt;Direct connection (5432)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrations (Prisma, Knex)&lt;/td&gt;
&lt;td&gt;Direct connection (5432)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Prisma gotcha
&lt;/h2&gt;

&lt;p&gt;Prisma uses prepared statements by default. The transaction pooler doesn't support prepared statements, so you'll get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: prepared statement "stmt_1" does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Add &lt;code&gt;?pgbouncer=true&lt;/code&gt; to your connection string, or use the direct connection for migrations and the pooler for runtime queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgresql://postgres.xxx:password@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick diagnosis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test if IPv6 works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# If this returns an address, IPv6 works&lt;/span&gt;
dig AAAA db.xxxxxxxxx.supabase.co

&lt;span class="c"&gt;# If this returns an address, IPv4 works&lt;/span&gt;
dig A db.xxxxxxxxx.supabase.co
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test the connection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Direct connection (may fail with IPv6)&lt;/span&gt;
psql &lt;span class="s2"&gt;"postgresql://postgres:password@db.xxxxxxxxx.supabase.co:5432/postgres"&lt;/span&gt;

&lt;span class="c"&gt;# Transaction pooler (should always work)&lt;/span&gt;
psql &lt;span class="s2"&gt;"postgresql://postgres.xxx:password@aws-0-us-east-1.pooler.supabase.com:6543/postgres"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my app work on Vercel but not locally?
&lt;/h3&gt;

&lt;p&gt;Vercel supports IPv6. Your local machine might not. Use the transaction pooler (6543) locally and you'll get the same behavior as production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I force IPv4 on the direct connection?
&lt;/h3&gt;

&lt;p&gt;Yes. Add &lt;code&gt;?sslmode=require&amp;amp;ip=4&lt;/code&gt; to the direct connection string, but this isn't officially supported and may break. Use the pooler instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between transaction and session pooler?
&lt;/h3&gt;

&lt;p&gt;Transaction pooler resets the connection after each transaction (faster, more scalable). Session pooler preserves session state (needed for &lt;code&gt;SET&lt;/code&gt; commands, &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;). Most apps should use transaction pooler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the pooler affect performance?
&lt;/h3&gt;

&lt;p&gt;Minimal. The pooler adds ~1-2ms of latency per query. For most web apps, this is negligible compared to network latency and query execution time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use both pooler and direct connections?
&lt;/h3&gt;

&lt;p&gt;Yes. Use the pooler for runtime queries (better scalability) and the direct connection for migrations and admin tasks (full protocol support).&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Schema Diagrams Go Stale (and the Fix)</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Tue, 22 Sep 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/why-schema-diagrams-go-stale-and-the-fix-3ib3</link>
      <guid>https://dev.to/not_varunkv/why-schema-diagrams-go-stale-and-the-fix-3ib3</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Schema diagrams go stale because they're static snapshots of a moving target. A migration adds a column, someone renames a table, and suddenly your beautiful diagram is wrong. The fix: generate diagrams from the live database, not from a file. Run it on every deploy or PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reason 1: Migrations don't update diagrams
&lt;/h3&gt;

&lt;p&gt;You run &lt;code&gt;ALTER TABLE orders ADD COLUMN shipping_cost_cents integer;&lt;/code&gt; and the migration succeeds. But your ER diagram still shows the old schema. Nobody thinks to update the diagram because the migration "just works."&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 2: Diagrams live in the wrong place
&lt;/h3&gt;

&lt;p&gt;If your diagram is in a Confluence page, a Figma file, or a static image in your repo, it's disconnected from the code. The code changes, the diagram doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 3: Nobody owns it
&lt;/h3&gt;

&lt;p&gt;Diagram maintenance falls between "frontend" and "backend" and "DevOps." Everyone assumes someone else will update it. Nobody does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix 1: Generate from the live database
&lt;/h3&gt;

&lt;p&gt;Instead of maintaining a static diagram, generate it from the actual database state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Using dbdiagramr CLI (or paste your connection string at dbdiagramr.space)&lt;/span&gt;
&lt;span class="c"&gt;# Your schema is always current -- no manual updates needed.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the nuclear option. Your diagram is always 100% accurate because it's generated from the source of truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2: Generate in CI/CD
&lt;/h3&gt;

&lt;p&gt;Add a schema diagram step to your CI pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions example&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate schema diagram&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;pg_dump --schema-only $DATABASE_URL &amp;gt; schema.sql&lt;/span&gt;
    &lt;span class="s"&gt;# Generate diagram from schema.sql&lt;/span&gt;
    &lt;span class="s"&gt;# Upload as artifact or commit to repo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on every push to &lt;code&gt;main&lt;/code&gt;. The diagram is always one commit behind, but it's close enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 3: One-page SCHEMA.md
&lt;/h3&gt;

&lt;p&gt;Maintain a single markdown file with the schema overview. Update it in the same PR that adds or changes a column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# SCHEMA.md&lt;/span&gt;

&lt;span class="gu"&gt;## orders&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`id`&lt;/span&gt; (uuid, PK)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`user_id`&lt;/span&gt; (uuid, FK → users)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`status`&lt;/span&gt; (text) -- pending | confirmed | shipped | delivered
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`total_amount_cents`&lt;/span&gt; (integer) -- added 2026-09-15
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`shipping_cost_cents`&lt;/span&gt; (integer) -- added 2026-09-20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put the update in your PR checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## PR checklist&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Tests pass
&lt;span class="p"&gt;-&lt;/span&gt; [ ] SCHEMA.md updated (if schema changed)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Migration is reversible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What doesn't work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manual updates to Figma/Confluence
&lt;/h3&gt;

&lt;p&gt;Nobody does it. The diagram is always 3 months out of date.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots of pgAdmin
&lt;/h3&gt;

&lt;p&gt;The moment you take the screenshot, it's stale. And you can't diff a screenshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-generated docs from migration files
&lt;/h3&gt;

&lt;p&gt;Migration files show the history of changes, not the current state. You'd need to replay all migrations to get the current schema, which is slow and error-prone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-healing diagram
&lt;/h2&gt;

&lt;p&gt;The ideal setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source of truth&lt;/strong&gt;: the live database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagram generation&lt;/strong&gt;: runs on every deploy (or on-demand).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: committed to the repo as an SVG or markdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforcement&lt;/strong&gt;: CI fails if the diagram doesn't match the schema.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is overkill for most projects. But if you've ever debugging a "why does the diagram show a column that doesn't exist?" issue, it's worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How often should I update my schema diagram?
&lt;/h3&gt;

&lt;p&gt;Every time you add, remove, or rename a column. If you're using a tool like dbdiagramr that generates from the live database, it's always current.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I commit the diagram to git?
&lt;/h3&gt;

&lt;p&gt;Yes. Commit it as an SVG or markdown file. That way it's versioned with your code and you can see when it changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the minimum viable schema documentation?
&lt;/h3&gt;

&lt;p&gt;A one-page &lt;code&gt;SCHEMA.md&lt;/code&gt; file in your repo with table names, key columns, and relationships. Update it in the same PR that changes the schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I auto-generate schema docs from Prisma/Drizzle schema files?
&lt;/h3&gt;

&lt;p&gt;Partially. Prisma's &lt;code&gt;prisma-docs-generator&lt;/code&gt; and Drizzle's &lt;code&gt;drizzle-kit generate&lt;/code&gt; can create basic docs. But they miss business rules, enum values, and deletion policies. Use them as a starting point, not the final doc.&lt;/p&gt;

</description>
      <category>database</category>
      <category>documentation</category>
      <category>tutorial</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How to Document Your Database Schema for a Team</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Thu, 17 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/how-to-document-your-database-schema-for-a-team-b8n</link>
      <guid>https://dev.to/not_varunkv/how-to-document-your-database-schema-for-a-team-b8n</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Document your schema in three layers: &lt;br&gt;
(1) name things well so they're self-documenting, &lt;br&gt;
(2) add inline column comments for the non-obvious stuff, &lt;br&gt;
(3) generate a one-page visual diagram that shows the relationships. Skip the 40-page Confluence page nobody reads it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 1: Name things well
&lt;/h2&gt;

&lt;p&gt;The fastest documentation is a good name. If your column names are clear, you barely need comments.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tables
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;plural nouns&lt;/strong&gt;: &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;order_items&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;snake_case&lt;/strong&gt;: &lt;code&gt;order_items&lt;/code&gt;, not &lt;code&gt;OrderItems&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Be specific: &lt;code&gt;payment_methods&lt;/code&gt;, not &lt;code&gt;methods&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Columns
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;updated_at&lt;/code&gt; for timestamps.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;{table}_id&lt;/code&gt; for foreign keys: &lt;code&gt;user_id&lt;/code&gt;, not &lt;code&gt;userId&lt;/code&gt; or &lt;code&gt;user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Prefix booleans with &lt;code&gt;is_&lt;/code&gt; or &lt;code&gt;has_&lt;/code&gt;: &lt;code&gt;is_active&lt;/code&gt;, &lt;code&gt;has_paid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Avoid abbreviations: &lt;code&gt;customer_id&lt;/code&gt;, not &lt;code&gt;cust_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Foreign keys
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Name them after the parent table: &lt;code&gt;orders.user_id&lt;/code&gt; → &lt;code&gt;users.id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Don't use generic names like &lt;code&gt;ref_id&lt;/code&gt; or &lt;code&gt;parent_id&lt;/code&gt; (unless it's a tree structure).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Layer 2: Inline comments
&lt;/h2&gt;

&lt;p&gt;PostgreSQL supports column-level comments. Use them for anything that isn't obvious from the name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'pending | confirmed | shipped | delivered | cancelled. Never delete orders set status to cancelled instead.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_number&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'E.164 format (+1234567890). Required for SMS auth. May be null if user signed up via OAuth.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'Stock Keeping Unit. Format: CATEGORY-NNNN (e.g., ELEC-0042). Must be unique across all products.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What to comment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enum values&lt;/strong&gt;: What are the possible values for &lt;code&gt;status&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format requirements&lt;/strong&gt;: What format does &lt;code&gt;phone_number&lt;/code&gt; expect?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business rules&lt;/strong&gt;: Why is &lt;code&gt;email&lt;/code&gt; nullable? (OAuth users may not share it.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deletion policy&lt;/strong&gt;: Do you delete rows or soft-delete?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What NOT to comment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; - everyone knows what &lt;code&gt;id&lt;/code&gt; is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; / &lt;code&gt;updated_at&lt;/code&gt; - self-explanatory.&lt;/li&gt;
&lt;li&gt;Foreign keys named after their parent table - &lt;code&gt;user_id&lt;/code&gt; is obvious.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layer 3: Visual diagram
&lt;/h2&gt;

&lt;p&gt;A one-page ER diagram replaces 40 pages of documentation. Generate it automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.dbdiagramr.space" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt;&lt;/strong&gt; - paste your connection string, get a visual schema in seconds. No signup required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pgAdmin&lt;/strong&gt; - built-in ERD tool for PostgreSQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DBeaver&lt;/strong&gt; - database tool with auto-generated ER diagrams.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What to show
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Table names and primary keys.&lt;/li&gt;
&lt;li&gt;Foreign key relationships (the lines between tables).&lt;/li&gt;
&lt;li&gt;Column types for non-obvious fields (JSONB, arrays, enums).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to skip
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Every single column - just show the important ones.&lt;/li&gt;
&lt;li&gt;Indexes - they're implementation details.&lt;/li&gt;
&lt;li&gt;Default values - put those in comments instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-page cheat sheet
&lt;/h2&gt;

&lt;p&gt;Create a single markdown file in your repo called &lt;code&gt;SCHEMA.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Database Schema&lt;/span&gt;

&lt;span class="gu"&gt;## Overview&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 12 tables, 3 schemas (public, auth, storage)
&lt;span class="p"&gt;-&lt;/span&gt; Last updated: 2026-09-01

&lt;span class="gu"&gt;## Tables&lt;/span&gt;

&lt;span class="gu"&gt;### users&lt;/span&gt;
Core user table. Created by Supabase Auth.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`id`&lt;/span&gt; (uuid, PK) - Auth user ID
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`email`&lt;/span&gt; (text) - May be null for phone auth
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`raw_user_meta_data`&lt;/span&gt; (jsonb) - Profile data from OAuth

&lt;span class="gu"&gt;### orders&lt;/span&gt;
Customer orders. Never delete - use status instead.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`id`&lt;/span&gt; (uuid, PK)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`user_id`&lt;/span&gt; (uuid, FK → users) - Customer
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`status`&lt;/span&gt; (text) - pending | confirmed | shipped | delivered | cancelled
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`total_amount_cents`&lt;/span&gt; (integer) - Price in cents to avoid float rounding

...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ER Diagram
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl10t584ocv71vqc8qhjf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl10t584ocv71vqc8qhjf.png" alt="Schema from dbdiagram" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming conventions by team size
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team size&lt;/th&gt;
&lt;th&gt;What works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 person&lt;/td&gt;
&lt;td&gt;Whatever you remember. Add comments for future-you.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-5 people&lt;/td&gt;
&lt;td&gt;Naming conventions + &lt;code&gt;SCHEMA.md&lt;/code&gt; cheat sheet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-15 people&lt;/td&gt;
&lt;td&gt;Naming conventions + comments + auto-generated docs (dbdiagramr, pgAdmin).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15+ people&lt;/td&gt;
&lt;td&gt;All of the above + dedicated data docs (DataHub, Atlan, or dbt docs).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I document every table?
&lt;/h3&gt;

&lt;p&gt;No. Document the tables that matter: the ones new engineers will touch, the ones with complex business logic, and the ones with non-obvious schemas. Skip internal Django/Laravel/NextAuth tables framework docs cover those.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I update the docs?
&lt;/h3&gt;

&lt;p&gt;When you add or change a column. The best way to enforce this: put the &lt;code&gt;SCHEMA.md&lt;/code&gt; update in your PR template as a checklist item.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the best tool for auto-generating schema docs?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.dbdiagramr.space" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt; for visual diagrams. For text-based docs, use dbt's &lt;code&gt;docs generate&lt;/code&gt; if you're already on dbt. For everything else, a markdown file in your repo beats any SaaS tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I generate schema docs from migration files?
&lt;/h3&gt;

&lt;p&gt;Partially. Migration files show the changes, not the current state. You'd need to run &lt;code&gt;pg_dump --schema-only&lt;/code&gt; or query &lt;code&gt;information_schema&lt;/code&gt; to get the actual current schema, then generate docs from that.&lt;/p&gt;

</description>
      <category>database</category>
      <category>documentation</category>
      <category>tutorial</category>
      <category>postgres</category>
    </item>
    <item>
      <title>information_schema vs pg_catalog: Which Should You Query?</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Tue, 15 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/informationschema-vs-pgcatalog-which-should-you-query-4e07</link>
      <guid>https://dev.to/not_varunkv/informationschema-vs-pgcatalog-which-should-you-query-4e07</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;information_schema&lt;/code&gt; is the SQL-standard way to query metadata portable across databases, slower, limited to what the standard defines. &lt;code&gt;pg_catalog&lt;/code&gt; is PostgreSQL-specific faster, more detailed, has everything. Use &lt;code&gt;information_schema&lt;/code&gt; for simple queries you might run on MySQL too. Use &lt;code&gt;pg_catalog&lt;/code&gt; when you need Postgres-specific details or performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each one is
&lt;/h2&gt;

&lt;h3&gt;
  
  
  information_schema
&lt;/h3&gt;

&lt;p&gt;A set of views defined by the SQL standard. Every relational database (Postgres, MySQL, SQL Server) has the same views with the same column names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Portable. Readable. Standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Slower. Missing Postgres-specific types (arrays, JSONB, ranges). Can't see internal system tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  pg_catalog
&lt;/h3&gt;

&lt;p&gt;PostgreSQL's internal catalog. It's a schema that's automatically search path and contains tables, views, and functions that describe every object in the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atttypid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atttypmod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_attribute&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attisdropped&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Faster. More detailed. Has Postgres-specific types. Can see system tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Not portable. Harder to read. Syntax is verbose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Head-to-head comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;information_schema&lt;/th&gt;
&lt;th&gt;pg_catalog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List tables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT table_name FROM information_schema.tables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT relname FROM pg_class WHERE relkind = 'r'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List columns&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT column_name, data_type FROM information_schema.columns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT attname, format_type(...) FROM pg_attribute&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List indexes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT indexname FROM information_schema.statistics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT indexrelname FROM pg_stat_user_indexes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List constraints&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT constraint_name FROM information_schema.table_constraints&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT conname FROM pg_constraint&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List foreign keys&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT ... FROM information_schema.key_column_usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT ... FROM pg_constraint WHERE contype = 'f'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table size&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT pg_total_relation_size(oid)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table owner&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT pg_catalog.get_owner(c.oid)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;View definitions&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT definition FROM pg_views&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use information_schema when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;portable SQL&lt;/strong&gt; (might run on MySQL/SQLite too).&lt;/li&gt;
&lt;li&gt;You're writing a &lt;strong&gt;simple query&lt;/strong&gt; and don't care about performance.&lt;/li&gt;
&lt;li&gt;You want &lt;strong&gt;readable, standard syntax&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You're building a tool that works across databases.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Simple: list all tables in the public schema&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;table_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'BASE TABLE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use pg_catalog when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;Postgres-specific info&lt;/strong&gt; (table owners, indexes, sizes, permissions).&lt;/li&gt;
&lt;li&gt;You're &lt;strong&gt;querying large databases&lt;/strong&gt; pg_catalog is faster.&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;internal system tables&lt;/strong&gt; (pg_stat_activity, pg_locks, etc.).&lt;/li&gt;
&lt;li&gt;You're building a &lt;strong&gt;Postgres-only tool&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- See active queries with full details&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;application_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common queries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  List all columns with types (information_schema)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_nullable&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  List all columns with types (pg_catalog)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atttypid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atttypmod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnotnull&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;nullable&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_attribute&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attisdropped&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  List foreign keys
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- information_schema&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;kcu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;kcu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ccu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;foreign_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ccu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;foreign_column&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;kcu&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_column_usage&lt;/span&gt; &lt;span class="n"&gt;ccu&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;kcu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ccu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;kcu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'FOREIGN KEY'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Show table sizes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- pg_catalog only&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  See who's connected right now
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- pg_catalog only&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;application_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;backend_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'client backend'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance note
&lt;/h2&gt;

&lt;p&gt;For simple metadata queries on small databases, the performance difference doesn't matter. But on large databases (thousands of tables, millions of rows), &lt;code&gt;pg_catalog&lt;/code&gt; can be 2-10x faster because it's indexed internally.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I query pg_catalog from MySQL?
&lt;/h3&gt;

&lt;p&gt;No. pg_catalog is PostgreSQL-specific. If you need cross-database portability, stick with &lt;code&gt;information_schema&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one does pg_dump use?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pg_catalog&lt;/code&gt;. It needs Postgres-specific details like table OIDs, ACLs, and storage parameters that &lt;code&gt;information_schema&lt;/code&gt; doesn't expose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do tools like Prisma or Drizzle use one over the other?
&lt;/h3&gt;

&lt;p&gt;They use both. Prisma uses &lt;code&gt;information_schema&lt;/code&gt; for schema introspection. Drizzle uses &lt;code&gt;pg_catalog&lt;/code&gt; for more detailed Postgres metadata. Both fall back to the other when one doesn't have the info they need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I see system tables (pg_stat_activity, pg_locks) from information_schema?
&lt;/h3&gt;

&lt;p&gt;No. System tables are only in &lt;code&gt;pg_catalog&lt;/code&gt;. Use &lt;code&gt;pg_catalog.pg_stat_activity&lt;/code&gt; to see active queries, &lt;code&gt;pg_catalog.pg_locks&lt;/code&gt; to see locks, etc.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>tutorial</category>
      <category>advanced</category>
    </item>
    <item>
      <title>Django Auth Tables and Permissions Explained</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:34:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/django-auth-tables-and-permissions-explained-44hd</link>
      <guid>https://dev.to/not_varunkv/django-auth-tables-and-permissions-explained-44hd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0h5fwft8rsrpxchgjafe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0h5fwft8rsrpxchgjafe.png" alt="DJANGO AUTH Schema Diagram built on DBDiagramr" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Django creates 5 core auth tables: &lt;code&gt;auth_user&lt;/code&gt;, &lt;code&gt;auth_group&lt;/code&gt;, &lt;code&gt;auth_permission&lt;/code&gt;, &lt;code&gt;django_content_type&lt;/code&gt;, and two join tables (&lt;code&gt;auth_user_groups&lt;/code&gt;, &lt;code&gt;auth_group_permissions&lt;/code&gt;). The permission system is built on content types each model gets a default &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;change&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, and &lt;code&gt;view&lt;/code&gt; permission, and you can create custom ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core tables
&lt;/h2&gt;

&lt;h3&gt;
  
  
  auth_user
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing primary key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;password&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(128)&lt;/td&gt;
&lt;td&gt;Hashed password (PBKDF2 by default).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;last_login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;datetime&lt;/td&gt;
&lt;td&gt;When the user last logged in. Null if never.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_superuser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;Bypasses all permission checks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;username&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(150)&lt;/td&gt;
&lt;td&gt;Unique username.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;first_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(150)&lt;/td&gt;
&lt;td&gt;User's first name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;last_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(150)&lt;/td&gt;
&lt;td&gt;User's last name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(254)&lt;/td&gt;
&lt;td&gt;Email address. Not necessarily unique.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_staff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;Can access the Django admin.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;Set to False instead of deleting users.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date_joined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;datetime&lt;/td&gt;
&lt;td&gt;Registration time.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  auth_group
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(150)&lt;/td&gt;
&lt;td&gt;Unique group name (e.g., "Editors", "Moderators").&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Groups are role containers. Assign permissions to groups, then add users to groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  auth_permission
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Human-readable name (e.g., "Can add post").&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content_type_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (FK)&lt;/td&gt;
&lt;td&gt;Which model this permission applies to.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codename&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(100)&lt;/td&gt;
&lt;td&gt;Code identifier (e.g., &lt;code&gt;add_post&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each model gets 4 default permissions: &lt;code&gt;add_modelname&lt;/code&gt;, &lt;code&gt;change_modelname&lt;/code&gt;, &lt;code&gt;delete_modelname&lt;/code&gt;, &lt;code&gt;view_modelname&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  django_content_type
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app_label&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(100)&lt;/td&gt;
&lt;td&gt;The Django app (e.g., &lt;code&gt;blog&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(100)&lt;/td&gt;
&lt;td&gt;The model name (e.g., &lt;code&gt;post&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table maps every model in your project to an ID. The permission system uses it to know which model a permission applies to.&lt;/p&gt;

&lt;h3&gt;
  
  
  auth_user_groups
&lt;/h3&gt;

&lt;p&gt;Join table for many-to-many: users ↔ groups.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (FK)&lt;/td&gt;
&lt;td&gt;→ &lt;code&gt;auth_user.id&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;group_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (FK)&lt;/td&gt;
&lt;td&gt;→ &lt;code&gt;auth_group.id&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  auth_group_permissions
&lt;/h3&gt;

&lt;p&gt;Join table for many-to-many: groups ↔ permissions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;group_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (FK)&lt;/td&gt;
&lt;td&gt;→ &lt;code&gt;auth_group.id&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;permission_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int (FK)&lt;/td&gt;
&lt;td&gt;→ &lt;code&gt;auth_permission.id&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How permissions work
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User ──M──M── Group ──M──M── Permission ──M──1── ContentType
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;permission&lt;/strong&gt; is tied to a &lt;strong&gt;content type&lt;/strong&gt; (a specific model).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groups&lt;/strong&gt; collect permissions (e.g., "Editors" get &lt;code&gt;add_post&lt;/code&gt;, &lt;code&gt;change_post&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users&lt;/strong&gt; are added to groups to inherit their permissions.&lt;/li&gt;
&lt;li&gt;You can also assign permissions directly to users (bypass groups).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check permission
&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_perm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;blog.add_post&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Add user to group
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib.auth.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Group&lt;/span&gt;
&lt;span class="n"&gt;editors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Editors&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;editors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create custom permission
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib.auth.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Permission&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib.contenttypes.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ContentType&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;blog.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Post&lt;/span&gt;
&lt;span class="n"&gt;content_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_for_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;custom_perm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;codename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;publish_post&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Can publish post&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to change
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;role&lt;/code&gt; field&lt;/strong&gt; to &lt;code&gt;auth_user&lt;/code&gt; if you need simple role-based access (or use groups).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add an &lt;code&gt;avatar&lt;/code&gt; field&lt;/strong&gt; for profile pictures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create custom permissions&lt;/strong&gt; for fine-grained access control (e.g., &lt;code&gt;publish_post&lt;/code&gt;, &lt;code&gt;archive_post&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;unique=True&lt;/code&gt;&lt;/strong&gt; to &lt;code&gt;email&lt;/code&gt; if you want unique emails.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to leave alone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't modify &lt;code&gt;django_content_type&lt;/code&gt; it's managed by Django's migration framework.&lt;/li&gt;
&lt;li&gt;Don't change &lt;code&gt;auth_permission.codename&lt;/code&gt; it's referenced by &lt;code&gt;has_perm()&lt;/code&gt; and decorators.&lt;/li&gt;
&lt;li&gt;Don't delete &lt;code&gt;auth_user.is_active&lt;/code&gt; use it instead of deleting users (preserves foreign keys).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Django create all these tables automatically?
&lt;/h3&gt;

&lt;p&gt;Yes. Running &lt;code&gt;python manage.py migrate&lt;/code&gt; creates all auth tables. They're part of Django's built-in &lt;code&gt;django.contrib.auth&lt;/code&gt; app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Django without the permission system?
&lt;/h3&gt;

&lt;p&gt;Yes. Remove &lt;code&gt;django.contrib.auth&lt;/code&gt; from &lt;code&gt;INSTALLED_APPS&lt;/code&gt; and you lose the permission tables but keep the &lt;code&gt;auth_user&lt;/code&gt; model (or replace it entirely).&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between &lt;code&gt;is_superuser&lt;/code&gt; and &lt;code&gt;is_staff&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;is_superuser&lt;/code&gt; bypasses all permission checks. &lt;code&gt;is_staff&lt;/code&gt; only controls access to the Django admin. A superuser automatically has &lt;code&gt;is_staff=True&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I see my Django auth tables?
&lt;/h3&gt;

&lt;p&gt;Use &lt;a href="https://www.dbdiagramr.space" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt; paste your connection string and get a visual schema of your Django auth tables.&lt;/p&gt;

</description>
      <category>django</category>
      <category>database</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>NextAuth / Auth.js Database Schema Explained</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Mon, 07 Sep 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/nextauth-authjs-database-schema-explained-13d2</link>
      <guid>https://dev.to/not_varunkv/nextauth-authjs-database-schema-explained-13d2</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;NextAuth (now Auth.js) creates 4 tables in your database: &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;accounts&lt;/code&gt;, &lt;code&gt;sessions&lt;/code&gt;, and &lt;code&gt;verification_tokens&lt;/code&gt;. The &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;accounts&lt;/code&gt; tables have a one-to-one relationship via &lt;code&gt;accounts.user_id&lt;/code&gt;. Sessions link to users via &lt;code&gt;sessions.user_id&lt;/code&gt;. Verification tokens are short-lived and self-cleaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 tables
&lt;/h2&gt;

&lt;h3&gt;
  
  
  users
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text / UUID&lt;/td&gt;
&lt;td&gt;Primary key. Generated by NextAuth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Display name from the OAuth provider (Google, GitHub, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;User's email. May be null if the provider doesn't share it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email_verified&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When the email was verified. Null if never verified.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Profile picture URL from the provider.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When the user first signed in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updated_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;Last profile sync from the provider.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  accounts
&lt;/h3&gt;

&lt;p&gt;This table links a user to an OAuth provider. One user can have multiple accounts (e.g., Google + GitHub).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text / UUID&lt;/td&gt;
&lt;td&gt;Primary key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Foreign key → &lt;code&gt;users.id&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Always &lt;code&gt;"oauth"&lt;/code&gt; or &lt;code&gt;"oidc"&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"google"&lt;/code&gt;, &lt;code&gt;"github"&lt;/code&gt;, &lt;code&gt;"discord"&lt;/code&gt;, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;provider_account_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;The provider's unique ID for this user.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refresh_token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;OAuth refresh token (encrypted in production).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;access_token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;OAuth access token (encrypted in production).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expires_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;When the access token expires (Unix timestamp).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Usually &lt;code&gt;"Bearer"&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Permissions granted by the provider.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id_token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;OIDC ID token (if using OIDC).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session_state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Provider-specific session state.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  sessions
&lt;/h3&gt;

&lt;p&gt;Active sessions for each user. NextAuth creates a new row here on every sign-in.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text / UUID&lt;/td&gt;
&lt;td&gt;Primary key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session_token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;The session token stored in the user's cookie.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Foreign key → &lt;code&gt;users.id&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expires&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When this session expires.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  verification_tokens
&lt;/h3&gt;

&lt;p&gt;Short-lived tokens for email verification, password reset, etc. Self-cleaning old tokens are deleted automatically.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;identifier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Email or user ID the token is for.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;The actual token value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expires&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When this token expires.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How they connect
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users ──1──1── accounts
   │
   1
   │
   ∞
sessions

users ──1──∞── verification_tokens (via identifier)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;One user → one or more accounts (Google, GitHub, etc.)&lt;/li&gt;
&lt;li&gt;One user → many sessions (different devices/browsers)&lt;/li&gt;
&lt;li&gt;Verification tokens are temporary and don't have a foreign key&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to change
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;role&lt;/code&gt; column&lt;/strong&gt; to &lt;code&gt;users&lt;/code&gt; if you need role-based access control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;phone_number&lt;/code&gt; column&lt;/strong&gt; to &lt;code&gt;users&lt;/code&gt; if you're using SMS auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt &lt;code&gt;access_token&lt;/code&gt; and &lt;code&gt;refresh_token&lt;/code&gt;&lt;/strong&gt; in production NextAuth doesn't do this by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to leave alone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't modify the &lt;code&gt;verification_tokens&lt;/code&gt; table it's managed automatically.&lt;/li&gt;
&lt;li&gt;Don't change the &lt;code&gt;session_token&lt;/code&gt; format it's a signed JWT.&lt;/li&gt;
&lt;li&gt;Don't add indexes to &lt;code&gt;provider_account_id&lt;/code&gt; unless you're querying it directly (it's already unique).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does NextAuth store passwords?
&lt;/h3&gt;

&lt;p&gt;No. NextAuth is an OAuth-first library. It doesn't handle passwords. If you need email/password auth, use &lt;code&gt;next-auth/providers/credentials&lt;/code&gt; with bcrypt, or use a service like Clerk or Lucia.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I see what's in my NextAuth tables?
&lt;/h3&gt;

&lt;p&gt;Use &lt;a href="https://www.dbdiagramr.space" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt; paste your connection string and get a visual schema of your NextAuth tables in seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I add custom fields to the users table?
&lt;/h3&gt;

&lt;p&gt;Yes. Add columns to the &lt;code&gt;users&lt;/code&gt; table directly. NextAuth will ignore columns it doesn't know about, so you can safely add &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;phone_number&lt;/code&gt;, &lt;code&gt;preferences&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when a user deletes their account?
&lt;/h3&gt;

&lt;p&gt;NextAuth doesn't cascade deletes by default. You need to manually delete from &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;accounts&lt;/code&gt;, and &lt;code&gt;sessions&lt;/code&gt;. Or add &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; to your foreign key constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Auth.js the same as NextAuth?
&lt;/h3&gt;

&lt;p&gt;Yes. Auth.js is the rebranded version of NextAuth. The database schema is identical. If you're on NextAuth v4, you're using the same tables.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>auth</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Laravel Default Database Tables Explained</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/laravel-default-database-tables-explained-341n</link>
      <guid>https://dev.to/not_varunkv/laravel-default-database-tables-explained-341n</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl09jga0j5lfykqnq4btj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl09jga0j5lfykqnq4btj.png" alt="Laravel Schema Diagram" width="799" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A fresh Laravel install creates 7-8 tables depending on your packages. The core ones are &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;password_resets&lt;/code&gt;, &lt;code&gt;failed_jobs&lt;/code&gt;, and &lt;code&gt;personal_access_tokens&lt;/code&gt;. The &lt;code&gt;cache&lt;/code&gt;, &lt;code&gt;sessions&lt;/code&gt;, &lt;code&gt;jobs&lt;/code&gt;, and &lt;code&gt;batches&lt;/code&gt; tables are only created if you run the corresponding Artisan commands. None of them are sacred -- you can rename, extend, or replace any of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core tables
&lt;/h2&gt;

&lt;h3&gt;
  
  
  users
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bigint (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing primary key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;User's display name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Unique email address.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email_verified_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When email was verified. Null if unverified.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;password&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Hashed password (bcrypt). Never store plain text.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remember_token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(100)&lt;/td&gt;
&lt;td&gt;Token for "remember me" functionality.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;Registration time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updated_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;Last profile update.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  password_resets
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;The email requesting a reset.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;The reset token (hashed).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When the token was generated.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is self-cleaning -- old tokens are garbage collected.&lt;/p&gt;

&lt;h3&gt;
  
  
  failed_jobs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bigint (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uuid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Unique identifier for the job.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Queue connection that failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;queue name&lt;/td&gt;
&lt;td&gt;Which queue the job was on.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longText&lt;/td&gt;
&lt;td&gt;The job's serialized data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exception&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longText&lt;/td&gt;
&lt;td&gt;The full exception stack trace.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When it failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use this table to debug failed queue jobs. The &lt;code&gt;exception&lt;/code&gt; column has the full stack trace.&lt;/p&gt;

&lt;h3&gt;
  
  
  personal_access_tokens
&lt;/h3&gt;

&lt;p&gt;Created by Laravel Sanctum for API token authentication.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bigint (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tokenable_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;The model this token belongs to (e.g., &lt;code&gt;App\Models\User&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tokenable_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bigint&lt;/td&gt;
&lt;td&gt;The ID of that model.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Token name (e.g., "Mobile App").&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(60)&lt;/td&gt;
&lt;td&gt;The hashed token value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abilities&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;JSON array of allowed abilities (e.g., &lt;code&gt;["*"]&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;last_used_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When the token was last used.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expires_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When the token expires (null = never).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Optional tables
&lt;/h2&gt;

&lt;h3&gt;
  
  
  sessions
&lt;/h3&gt;

&lt;p&gt;Created by &lt;code&gt;php artisan session:table&lt;/code&gt;. Stores HTTP session data in the database instead of files.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255) (PK)&lt;/td&gt;
&lt;td&gt;Session ID (from the cookie).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bigint (FK)&lt;/td&gt;
&lt;td&gt;The user this session belongs to. Null for guests.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip_address&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(45)&lt;/td&gt;
&lt;td&gt;Client IP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;Browser user agent string.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longText&lt;/td&gt;
&lt;td&gt;Serialized session data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;last_activity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;Unix timestamp of last activity.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  cache
&lt;/h3&gt;

&lt;p&gt;Created by &lt;code&gt;php artisan cache:table&lt;/code&gt;. Stores key-value cache entries in the database.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255) (PK)&lt;/td&gt;
&lt;td&gt;Cache key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longText&lt;/td&gt;
&lt;td&gt;Serialized cache value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expiration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;Unix timestamp when it expires.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  jobs
&lt;/h3&gt;

&lt;p&gt;Created by &lt;code&gt;php artisan queue:table&lt;/code&gt;. Stores pending queue jobs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bigint (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Queue name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longText&lt;/td&gt;
&lt;td&gt;Serialized job data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attempts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;How many times it's been tried.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reserved_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;When it was reserved by a worker.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;available_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;When it can be picked up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;When it was dispatched.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  batches
&lt;/h3&gt;

&lt;p&gt;Created by &lt;code&gt;php artisan queue:batches-table&lt;/code&gt;. Tracks batch progress for parallel job processing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bigint (PK)&lt;/td&gt;
&lt;td&gt;Auto-incrementing ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(255)&lt;/td&gt;
&lt;td&gt;Batch name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;total_jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;Total jobs in the batch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pending_jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;Jobs still to run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed_jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;Jobs that failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed_job_ids&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longText&lt;/td&gt;
&lt;td&gt;JSON array of failed job IDs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;options&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longText&lt;/td&gt;
&lt;td&gt;Serialized batch options.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cancelled_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When cancelled (null if not).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When the batch was created.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;finished_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;td&gt;When the batch completed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What to change
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;role&lt;/code&gt; column&lt;/strong&gt; to &lt;code&gt;users&lt;/code&gt; for role-based access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;phone_number&lt;/code&gt; column&lt;/strong&gt; for SMS auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add an &lt;code&gt;avatar_url&lt;/code&gt; column&lt;/strong&gt; for profile pictures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename &lt;code&gt;password_resets&lt;/code&gt;&lt;/strong&gt; to &lt;code&gt;password_reset_tokens&lt;/code&gt; (Laravel 8+ convention).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to leave alone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't modify the &lt;code&gt;failed_jobs.exception&lt;/code&gt; column -- it needs to hold full stack traces.&lt;/li&gt;
&lt;li&gt;Don't remove &lt;code&gt;personal_access_tokens.tokenable_type&lt;/code&gt; -- it's a polymorphic relation.&lt;/li&gt;
&lt;li&gt;Don't add indexes to &lt;code&gt;cache.key&lt;/code&gt; -- it's already the primary key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Laravel create all these tables automatically?
&lt;/h3&gt;

&lt;p&gt;No. Only &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;password_resets&lt;/code&gt;, and &lt;code&gt;failed_jobs&lt;/code&gt; are created by &lt;code&gt;php artisan migrate&lt;/code&gt;. The others (&lt;code&gt;sessions&lt;/code&gt;, &lt;code&gt;cache&lt;/code&gt;, &lt;code&gt;jobs&lt;/code&gt;, &lt;code&gt;batches&lt;/code&gt;) require separate Artisan commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Redis instead of the database tables?
&lt;/h3&gt;

&lt;p&gt;Yes. Laravel supports Redis for sessions, cache, and queues out of the box. Switch your &lt;code&gt;.env&lt;/code&gt; driver to &lt;code&gt;redis&lt;/code&gt; and the database tables become unnecessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between &lt;code&gt;password_resets&lt;/code&gt; and &lt;code&gt;password_reset_tokens&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Same table, different names. Laravel 8+ renamed it to &lt;code&gt;password_reset_tokens&lt;/code&gt;. If you're on an older version, it's still &lt;code&gt;password_resets&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I visualize my Laravel schema?
&lt;/h3&gt;

&lt;p&gt;Use &lt;a href="https://www.dbdiagramr.space" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt; -- paste your connection string and get a visual schema of all your Laravel tables.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>database</category>
      <category>tutorial</category>
      <category>php</category>
    </item>
    <item>
      <title>What Is an ER Diagram and How to Read One</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Thu, 03 Sep 2026 03:00:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/what-is-an-er-diagram-and-how-to-read-one-23h9</link>
      <guid>https://dev.to/not_varunkv/what-is-an-er-diagram-and-how-to-read-one-23h9</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;An entity-relationship diagram is a picture of your database's tables, columns, and connections. You read it left to right: boxes are tables, lines are foreign-key relationships, and the symbols on each end tell you one-to-one, one-to-many, or many-to-many.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each part means
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Entities (tables)
&lt;/h3&gt;

&lt;p&gt;Every box is a table. The bold name on top is the table name. The list underneath is the columns. Primary keys are usually marked with a key icon or bolded. Foreign keys have a link icon.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;id&lt;/strong&gt; (PK)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;created_at&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Attributes (columns)
&lt;/h3&gt;

&lt;p&gt;Each line under the table name is a column. Common types you'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; -- primary key (unique identifier)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt; -- simple text fields&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; -- timestamp of when the row was inserted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_id&lt;/code&gt; -- foreign key pointing to another table (look for the line connecting it)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Relationships (lines)
&lt;/h3&gt;

&lt;p&gt;Lines between boxes represent foreign key references. If you see a line from &lt;code&gt;orders.user_id&lt;/code&gt; to &lt;code&gt;users.id&lt;/code&gt;, that means "each order belongs to one user."&lt;/p&gt;

&lt;h3&gt;
  
  
  Cardinality (symbols)
&lt;/h3&gt;

&lt;p&gt;The symbols at the end of each line tell you how many:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 -- ∞&lt;/strong&gt; (one to many): one user has many orders. This is the most common.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 -- 1&lt;/strong&gt; (one to one): one user has one profile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;∞ -- ∞&lt;/strong&gt; (many to many): many users have many roles. Usually resolved with a join table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reading a real example
&lt;/h2&gt;

&lt;p&gt;Suppose you see this diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users ──1──∞── orders ──1──∞── order_items ──∞──1── products
   │
   1
   │
   ∞
addresses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading left to right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One &lt;strong&gt;user&lt;/strong&gt; has many &lt;strong&gt;orders&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;One &lt;strong&gt;order&lt;/strong&gt; has many &lt;strong&gt;order items&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;One &lt;strong&gt;product&lt;/strong&gt; appears in many &lt;strong&gt;order items&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;One &lt;strong&gt;user&lt;/strong&gt; has many &lt;strong&gt;addresses&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The join table here is &lt;code&gt;order_items&lt;/code&gt; ,it connects &lt;code&gt;orders&lt;/code&gt; and &lt;code&gt;products&lt;/code&gt; in a many-to-many relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ER diagrams matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debugging&lt;/strong&gt;: "Why is my query returning duplicates?" look for a missing join or unintended many-to-many.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding&lt;/strong&gt;: New engineers understand the data model in 5 minutes instead of reading migration files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema reviews&lt;/strong&gt;: Spot missing indexes, redundant tables, or circular dependencies before they hit production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assuming every line is one-to-many.&lt;/strong&gt; Check the symbols. A many-to-many without a join table is a red flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring nullable columns.&lt;/strong&gt; A dashed line or optional symbol means the relationship is optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to check the foreign key direction.&lt;/strong&gt; &lt;code&gt;orders.user_id → users.id&lt;/code&gt; is different from &lt;code&gt;users.order_id → orders.id&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Open your own database schema in &lt;a href="https://www.dbdiagramr.space" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt; and trace the foreign keys. Start from the table you're most familiar with and follow the lines outward.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does PK mean in an ER diagram?
&lt;/h3&gt;

&lt;p&gt;PK stands for primary key the unique identifier for each row in a table. It's usually &lt;code&gt;id&lt;/code&gt; and is referenced by foreign keys in other tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does FK mean?
&lt;/h3&gt;

&lt;p&gt;FK stands for foreign key a column in one table that points to the primary key of another table. It creates the relationship between the two tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you show a many-to-many relationship?
&lt;/h3&gt;

&lt;p&gt;With a join table. For example, &lt;code&gt;users&lt;/code&gt; ↔ &lt;code&gt;user_roles&lt;/code&gt; ↔ &lt;code&gt;roles&lt;/code&gt;. The &lt;code&gt;user_roles&lt;/code&gt; table holds &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;role_id&lt;/code&gt; foreign keys, and the many-to-many becomes two one-to-many relationships.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between an ER diagram and a schema diagram?
&lt;/h3&gt;

&lt;p&gt;They're the same thing in practice. ER diagram is the academic term; schema diagram is the engineering term. Both show tables, columns, and relationships.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to draw an ER diagram by hand?
&lt;/h3&gt;

&lt;p&gt;No. Tools like &lt;a href="https://www.dbdiagramr.space" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt; generate ER diagrams automatically from your Supabase, Neon, or PostgreSQL connection string. Paste your connection string and get a visual schema in seconds.&lt;/p&gt;

</description>
      <category>database</category>
      <category>tutorial</category>
      <category>postgres</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Supabase Auth Schema Explained: Users, Identities, Sessions</title>
      <dc:creator>Varun Krishnan</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/not_varunkv/supabase-auth-schema-explained-users-identities-sessions-5b88</link>
      <guid>https://dev.to/not_varunkv/supabase-auth-schema-explained-users-identities-sessions-5b88</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Supabase stores auth in a separate &lt;code&gt;auth&lt;/code&gt; schema not your &lt;code&gt;public&lt;/code&gt; schema. Every user has exactly &lt;strong&gt;one row in &lt;code&gt;auth.users&lt;/code&gt;&lt;/strong&gt;, one or more rows in &lt;strong&gt;&lt;code&gt;auth.identities&lt;/code&gt;&lt;/strong&gt; (one per login provider), and can hold &lt;strong&gt;multiple active &lt;code&gt;auth.sessions&lt;/code&gt;&lt;/strong&gt;, each backed by a &lt;code&gt;refresh_tokens&lt;/code&gt; row. If you've ever poked at a Supabase database and seen a cluster of tables you didn't create, these are the ones, and this is what they do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth.users 1───* auth.identities
auth.users 1───* auth.sessions
auth.sessions 1───* auth.refresh_tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why an &lt;code&gt;auth&lt;/code&gt; schema at all
&lt;/h2&gt;

&lt;p&gt;Supabase deliberately keeps auth separate from your app's tables. Your &lt;code&gt;public&lt;/code&gt; schema is where your own models live; &lt;code&gt;auth&lt;/code&gt; is locked down and managed by the auth service (GoTrue). You read from it, you don't write to it. That separation is why your migrations never touch these tables and why introspecting a Supabase database shows you a schema you didn't write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four tables that matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;auth.users&lt;/code&gt; one row per user.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UUID primary key; the user's stable identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;email&lt;/code&gt; / &lt;code&gt;phone&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Contact + login identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encrypted_password&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bcrypt hash (only for email/password auth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;raw_app_meta_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provider claims: &lt;code&gt;provider&lt;/code&gt;, &lt;code&gt;providers&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;raw_user_meta_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom metadata you set on the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_sso_user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;True when sign-in came through SSO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;confirmed_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the primary identity was confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;auth.identities&lt;/code&gt; - one row per login method.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user signing in with email &lt;strong&gt;and&lt;/strong&gt; GitHub gets two rows here. &lt;code&gt;provider_id&lt;/code&gt; is the provider's identifier for that user; &lt;code&gt;identity_data&lt;/code&gt; is the raw claim payload (name, avatar, email) the provider returned. The FK &lt;code&gt;user_id → users.id&lt;/code&gt; is what joins them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;auth.sessions&lt;/code&gt; - a browser/token session.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One session per logged-in device roughly. &lt;code&gt;aal&lt;/code&gt; (assurance level), &lt;code&gt;user_agent&lt;/code&gt;, &lt;code&gt;ip&lt;/code&gt;, and &lt;code&gt;not_after&lt;/code&gt; all live here. &lt;code&gt;factor_id&lt;/code&gt; links to MFA factors when you have TOTP enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;auth.refresh_tokens&lt;/code&gt; - the long-lived token backing a session.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Access tokens are short (JWT, ~1h). Refresh tokens are long and stored here, &lt;code&gt;revoked&lt;/code&gt; flag included, with &lt;code&gt;parent&lt;/code&gt; used to detect token reuse and rotate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they relate (the joins you'll actually write)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identities&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt;  &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That query is the 80% case: "who is signed in, through which provider, on which sessions." Every relationship is a plain foreign key &lt;code&gt;identities.user_id&lt;/code&gt;, &lt;code&gt;sessions.user_id&lt;/code&gt;, &lt;code&gt;refresh_tokens.session_id&lt;/code&gt; which is exactly what a schema diagram turns into readable arrows. If you'd rather trace them visually, paste a read-only connection string into &lt;a href="https://dbdiagramr.space/visualize" rel="noopener noreferrer"&gt;dbdiagramr&lt;/a&gt; and it will pull and render the same tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's usually NOT your business
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;auth.instances&lt;/code&gt;, &lt;code&gt;auth.audit_log_entries&lt;/code&gt;, and &lt;code&gt;auth.schema_migrations&lt;/code&gt; are Supabase's own bookkeeping. &lt;code&gt;audit_log_entries&lt;/code&gt; records admin actions inside the auth service; &lt;code&gt;instances&lt;/code&gt; is leftover multi-tenancy plumbing. If a diagram shows them, ignore them your reads live in the other four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never write to &lt;code&gt;auth&lt;/code&gt; tables directly.&lt;/strong&gt; Use the Supabase client / Admin API. Direct inserts create inconsistent state (orphaned identities, unhashed passwords).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreign-key joins work across schemas.&lt;/strong&gt; &lt;code&gt;auth.users.id&lt;/code&gt; is the same UUID you'd use in &lt;code&gt;public&lt;/code&gt; join &lt;code&gt;public.profiles.user_id → auth.users.id&lt;/code&gt; for your own profile data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A user with no identity row is a sign of trouble.&lt;/strong&gt; Every normal user has at least one. Orphaned &lt;code&gt;identities&lt;/code&gt; without a &lt;code&gt;users&lt;/code&gt; row point at a cleanup/import bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions accumulate.&lt;/strong&gt; Old sessions linger; your diagram showing many sessions per user isn't a leak, it's devices and tabs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This whole picture is easier to keep straight when you can see it. The &lt;a href="https://dbdiagramr.space/schema/supabase" rel="noopener noreferrer"&gt;Supabase auth schema diagram&lt;/a&gt; was generated by introspecting a live Supabase database users, identities, sessions, and refresh_tokens with their foreign keys rendered as relationships. Open it next time you're debugging a sign-in flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where is the Supabase auth schema?&lt;/strong&gt;&lt;br&gt;
In the &lt;code&gt;auth&lt;/code&gt; schema, separate from your &lt;code&gt;public&lt;/code&gt; schema &lt;code&gt;auth.users&lt;/code&gt;, &lt;code&gt;auth.identities&lt;/code&gt;, &lt;code&gt;auth.sessions&lt;/code&gt;, &lt;code&gt;auth.refresh_tokens&lt;/code&gt;, plus internal tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is &lt;code&gt;auth.users&lt;/code&gt; used for?&lt;/strong&gt;&lt;br&gt;
It's the single source of truth for who can sign in. One row per user, with email/phone, password hash, metadata, and provider flags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does one user have multiple identities?&lt;/strong&gt;&lt;br&gt;
Each login method is a separate &lt;code&gt;auth.identities&lt;/code&gt; row. Email + GitHub + Google = three rows, all pointing at the same &lt;code&gt;users.id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between a session and a refresh token?&lt;/strong&gt;&lt;br&gt;
A session is the device/token context; the refresh token is the long-lived credential that renews the short-lived JWT. One session maps to one refresh token chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I join auth tables to my public tables?&lt;/strong&gt;&lt;br&gt;
Yes the auth schema isn't isolated for queries. Use &lt;code&gt;auth.users.id&lt;/code&gt; as the join key with your &lt;code&gt;public.*&lt;/code&gt; tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Live: &lt;a href="https://dbdiagramr.space" rel="noopener noreferrer"&gt;https://dbdiagramr.space&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/VarunKvK/dbdiagramr" rel="noopener noreferrer"&gt;https://github.com/VarunKvK/dbdiagramr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this is useful to you, a GitHub star helps a solo dev keep building in public. I started this tool because I kept needing to visualize exactly these tables after reading migrations.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
