DEV Community

01kg
01kg

Posted on

Supabase | My Way of Designing & Managing DB

Look before you leap.
-- Aesop's fable

1. Design DB relations on dbdiagram.io

Tinkering SQL code is hard, a website like dbdiagram.io makes my life easier.

dbdiagram.io export to Postgres

⬆️ Export to Postgres after designing.

2. Move dbdiagram.io exported .sql file to supabase > migrations folder

Rename it if you like.

a screen shot of supabase folder structure

Note: If your DB topology designed in dbdiagram.io involved tables that do not belong to 'public' schema, check the code in .sql file before using. Let's say it involved "users" in "auth" schema, the code would contain CREATE TABLE "users" which is unnecessary.

3. Let local Supabase DB apply the migration.

supabase db reset --no-seed

4. Leverage the power of seeding tools such as Snaplet Seed

I'd like to share my seed.config.ts and seed.ts file for your reference.

⬇️seed.config.ts

// seed.config.ts
import { SeedPostgres } from "@snaplet/seed/adapter-postgres";
import { defineConfig } from "@snaplet/seed/config";
import postgres from "postgres";

export default defineConfig({
  adapter: () => {
    const client = postgres(
      //// Local dev
      "postgresql://postgres:postgres@127.0.0.1:54322/postgres",
      //// Prod
      // "postgresql://YOUR_PROD_DATABASE_URL/postgres",
    );
    return new SeedPostgres(client);
  },
  select: [
    // We don't alter any extensions tables that might be owned by extensions
    "!*",
    // We want to alter all the tables under public schema
    "public*",
    // We also want to alter some of the tables under the auth schema
    "auth.users",
    "auth.identities",
    "auth.sessions",
  ],
});
Enter fullscreen mode Exit fullscreen mode

⬇️seed.ts (the code will create a user for testing, refactor it to suit your need.)

// seed.ts
import { createSeedClient } from "@snaplet/seed";
import { v4 as uuidv4 } from "uuid";
import bcrypt from "bcrypt";
async function hashPassword(password: string): Promise<string> {
  const saltRounds = 10; // You can adjust the salt rounds as needed
  const salt = await bcrypt.genSalt(saltRounds);
  const hashedPassword = await bcrypt.hash(password, salt);
  return hashedPassword;
}

const DRY_RUN = false;

const USER_EMAIL = "my@user.com";
const USER_PASSWORD = "userpass";

const main = async () => {
  const USER_ENCRYPTED_PASSWORD = await hashPassword(USER_PASSWORD);
  const USER_ID = uuidv4();

  const seed = await createSeedClient({ dryRun: DRY_RUN });

  // Truncate all tables in the database
  await seed.$resetDatabase();

  // Create the user and its related data
  await seed.auth_users([
    {
      id: USER_ID,
      instance_id: "00000000-0000-0000-0000-000000000000",
      aud: "authenticated",
      role: "authenticated",
      email: USER_EMAIL,
      encrypted_password: USER_ENCRYPTED_PASSWORD,
      // email_confirmed_at: "", // Snaplet will generate this for you
      invited_at: null,
      confirmation_token: "",
      confirmation_sent_at: null,
      recovery_token: "",
      recovery_sent_at: null,
      email_change_token_new: "",
      email_change: "",
      email_change_sent_at: null,
      // last_sign_in_at: "", // Snaplet will generate this for you
      raw_app_meta_data: { "provider": "email", "providers": ["email"] },
      raw_user_meta_data: {
        "sub": USER_ID,
        "email": USER_EMAIL,
        "email_verified": false,
        "phone_verified": false,
      },
      is_super_admin: null,
      // created_at: "", // Snaplet will generate this for you
      // updated_at: "", // Snaplet will generate this for you
      phone: null,
      phone_confirmed_at: null,
      phone_change: "",
      phone_change_token: "",
      phone_change_sent_at: null,
      email_change_token_current: "",
      email_change_confirm_status: 0,
      banned_until: null,
      reauthentication_token: "",
      reauthentication_sent_at: null,
      is_sso_user: false,
      deleted_at: null,
      is_anonymous: false,

      identities: [{
        // id: "", // Snaplet will generate this for you
        identity_data: {
          "sub": USER_ID,
          "email": USER_EMAIL,
          "email_verified": false,
          "phone_verified": false,
        },
        provider: "email",
        provider_id: USER_ID, // If the provider is email or phone, the id is the user's id from the auth.users table. https://supabase.com/docs/guides/auth/identities#the-user-identity-object
        // last_sign_in_at: "", // Snaplet will generate this for you
        // created_at: "", // Snaplet will generate this for you
        // updated_at: "", // Snaplet will generate this for you
      }],
      // data related to user
      // ...
    },
  ]);

  if (!DRY_RUN) {
    console.log("Database seeded successfully!");
  }

  process.exit();
};

main();
Enter fullscreen mode Exit fullscreen mode

- 4.1 Let Snaplet Seed get to know the current status of your local DB

npx @snaplet/seed sync

Note: Make sure seed.config.ts contains your local DB link:

// seed.config.ts
...
  adapter: () => {
    const client = postgres(
      //// Local dev
      "postgresql://postgres:postgres@127.0.0.1:54322/postgres",
      //// Prod
      // "postgresql://YOUR_PROD_DATABASE_URL/postgres",
...
Enter fullscreen mode Exit fullscreen mode

- 4.2 Let Snaplet Seed seed the data into local DB, not dry-run to seed.sql

In my practical learning, dry-run to generate and pour SQL commands into seed.sql is not convenient due to this issue I mentioned before. So I let Snaplet Seed seed data directly into local DB. (I use dry-run only for debugging purpose sometime.)

Make sure seed.ts sets DRY_RUN to false:

// seed.ts
...
const DRY_RUN = false;
...
Enter fullscreen mode Exit fullscreen mode

Run npx tsx seed.ts to seed data into local DB.

5. Reset remote DB

Let's make it clear:

  • The topology designed on dbdiagram.io is the source of truth for local DB's structure.

  • The local DB's structure is the source of truth for remote db.

After local DB's structure is stable, it's time to sync the initial status of remote DB with local.

Run supabase db reset --linked --no-seed

6. (Optional) Seed data to remote DB

Prerequisites

1. Use prod DB link in seed.config.ts

// seed.config.ts
...
  adapter: () => {
    const client = postgres(
      //// Local dev
      // "postgresql://postgres:postgres@127.0.0.1:54322/postgres",
      //// Prod
      "postgresql://YOUR_PROD_DATABASE_URL/postgres",
...
Enter fullscreen mode Exit fullscreen mode

2. Make sure seed.ts sets DRY_RUN to false

// seed.ts
...
const DRY_RUN = false;
...
Enter fullscreen mode Exit fullscreen mode

Run npx tsx seed.ts to seed data into remote DB.

Top comments (0)