howto

No build static site that used supabase

who needs tooling

tags
supabase
nobuild
static_site

Setup supabase

If you don't have the supabase cli, install it now.

1
  brew install supabase/tap/supabase

Then in a new directory, initialize the project and start it up locally.

1
2
  supabase init
  supabase start

Website

Lets put together a simple html page. We'll include shoelace for a nice design system and some components.

We'll also put in profile-panel, post-list, and post-form, which we will implement below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>My awesome project</title>
      <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/cdn/themes/light.css"
      />
      <script
        type="module"
        src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/cdn/shoelace-autoloader.js"
      ></script>
      <link rel="stylesheet" href="styles.css" />
    </head>
    <body>
      <header>
        <h1>Awesome project</h1>
        <profile-panel></profile-panel>
      </header>
      <main>
        <section id="post">
          <post-list></post-list>
        </section>
        <section id="post-form">
          <post-form></post-form>
        </section>
      </main>
      <footer>
        <h2>Its amazing</h2>
      </footer>

      <script src="scripts.js" type="module"></script>
    </body>
  </html>

Styles

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  body {
      font-family: var(--sl-font-sans);
      max-width: 1000px;
      margin: 0 auto;
      padding: 0 var(--sl-spacing-medium);
  }

  h1 {
      font-size: var(--sl-font-size-3x-large);
  }

  h2 {
      font-size: var(--sl-font-size-2x-large);
  }

  h3 {
      font-size: var(--sl-font-size-x-large);
  }

  h4 {
      font-size: var(--sl-font-size-large);
  }

  p,
  ul,
  ol {
      font-size: var(--sl-font-size-large);
  }

  header {
      display: flex;
      justify-content: space-between;
      align-items: center;
  }

  section#post-form {
      max-width: 65ch;
      margin: 0 auto;
  }

  .sl-toast-stack {
      left: 0;
      right: auto;
  }

Start it up

1
  pnpx live-server

Which looks like:

Setup supabase

For our local build, we can see what the settings are by:

1
supabase status -o env | cut -c1-80
ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwic
API_URL="http://127.0.0.1:54321"
DB_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
GRAPHQL_URL="http://127.0.0.1:54321/graphql/v1"
INBUCKET_URL="http://127.0.0.1:54324"
JWT_SECRET="super-secret-jwt-token-with-at-least-32-characters-long"
S3_PROTOCOL_ACCESS_KEY_ID="625729a08b95bf1b7ff351a663f3a23c"
S3_PROTOCOL_ACCESS_KEY_SECRET="850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed
S3_PROTOCOL_REGION="local"
SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZ
STORAGE_S3_URL="http://127.0.0.1:54321/storage/v1/s3"
STUDIO_URL="http://127.0.0.1:54323"

We'll need the API_URL and the ANON_KEY

Lets set up db.js

1
2
3
4
5
6
7
  // db.js
  import { createClient } from "https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm";

  let api_url="http://127.0.0.1:54321";
  let anon_key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"

  export const supabase = createClient(api_url, anon_key);

And then add a notify function so we can pop stuff up on the screen and annoy people:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  // notify.js
  // Always escape HTML for text arguments!
  export function escapeHtml(html) {
      const div = document.createElement("div");
      div.textContent = html;
      return div.innerHTML;
  }

  // Custom function to emit toast notifications
  export async function notify(
      message,
      variant = "primary",
      icon = "info-circle",
      duration = 3000
  ) {
      const alert = Object.assign(document.createElement("sl-alert"), {
          variant,
          closable: true,
          duration: duration,
          innerHTML: `
          <sl-icon name="${icon}" slot="icon"></sl-icon>
          ${escapeHtml(message)}
        `,
      });
      document.body.append(alert);
      setTimeout(() => {
          alert.toast();
      }, 250);
  }

profile-panel.js

This component handles all of the login state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  import { supabase } from "./db.js";
  import { notify } from "./notify.js";

  class ProfilePanel extends HTMLElement {
    constructor() {
      super();
      this.state = "anonymous";
    }

    connectedCallback() {
      const { localdata } = supabase.auth.onAuthStateChange((event, session) => {
        notify(event);
        if (event == "SIGNED_IN") {
          this.state = "authenticated";
          this.session = session;
          console.log("session", session);
        } else if (event == "SIGNED_OUT") {
          this.state = "anonymous";
          this.session = null;
        }
        this.render();
      });

      this.data = localdata;

      this.render();
    }

    disconnectedCallback() {
      this.data.subscription.unsubscribe();
    }

      render() {
          if( this.state != 'authenticated' ) {
              this.innerHTML = '<anonymous-profile>Anonymous</anonymous-profile>';
          } else {
              this.innerHTML = `<authed-profile email="${this.session.user.email}">Logged in</authed-profile>`
          }
      }
  }

  customElements.define("profile-panel", ProfilePanel);

And install it in scripts.js:

1
  import './profile-panel.js';

anonymous-profile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
  import {supabase} from './db.js';
  import {notify} from './notify.js'

  class AnonymousProfile extends HTMLElement {
      constructor() {
          super();
          this.error = ""
      }

      connectedCallback() {
          this.render();
      }

      render() {
          const errorMessage = this.error != "" ? `<p>${this.error}</p>` : '';

          this.innerHTML = `<sl-button variant="primary" id="sign-in">Sign In</sl-button>
  <sl-dialog label="Signin" class="dialog-overview">
  ${errorMessage}
      <form>
        <sl-input id="email" name="email" label="Email" required></sl-input>
        <sl-input id="password" name="password" label="Password" type="password" required></sl-input>
        <sl-button variant="primary" id="signin">Signin</sl-button>
        <sl-button  variant="primary" id="login">Login</sl-button>
      </form>

  </sl-dialog>
  `;

      this.querySelector("#sign-in").addEventListener("click", () => {
        this.querySelector("sl-dialog").show();
        this.querySelector("#signin").addEventListener("click", (event) => {
          this.handleSignin(event);
        });
        this.querySelector("#login").addEventListener("click", (event) => {
          this.handleLogin(event);
        });
      });
    }

    async handleSignin(event) {
        this.error = "";
        event.preventDefault();
        const formData = new FormData(this.querySelector("form"));
        
        const email = formData.get("email");
        const password = formData.get("password");
        const { data, error } = await supabase.auth.signUp({
            email,
            password,
        });
        
        if (error) {
            this.error = error.message;
            notify(error.message, "danger");
            this.render();
            this.querySelector("sl-dialog").show();

        }
    }
      
      async handleLogin(event) {
          this.error = "";
          event.preventDefault();
          const formData = new FormData(this.querySelector("form"));
          const email = formData.get("email");
          const password = formData.get("password");

          const { data, error } = await supabase.auth.signInWithPassword({
              email,
              password,
          });
          
          if (error) {
              this.error = error.message;
              notify(error.message, "danger");
              this.render();
              this.querySelector("sl-dialog").show();

          }
      }

  }

  customElements.define("anonymous-profile", AnonymousProfile );

And install it in scripts.js:

1
  import './anonymous-profile.js';

And

authed-profile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  import { supabase } from "./db.js";
  import { notify } from "./notify.js";

  class AuthedProfile extends HTMLElement {
      constructor() {
          super();
      }

      connectedCallback() {
          this.innerHTML = `
  <sl-dropdown class="profile-dropdown">
    <sl-avatar slot="trigger"></sl-avatar>
    <sl-menu>
      <sl-menu-item>${this.getAttribute("email")}</sl-menu-item>
      <sl-divider></sl-divider>
      <sl-menu-item id="logout">Logout</sl-menu-item>
    </sl-menu>
  </sl-dropdown>
  `;

          const menu = document.querySelector(".profile-dropdown");

          menu.addEventListener("sl-select", (event) => {
              console.log("sl-select", event.detail.item);
              if (event.detail.item.id === "logout") {
                  supabase.auth.signOut();
              }
          });
      }
  }

  customElements.define("authed-profile", AuthedProfile);

And add it to scripts.js:

1
  import './authed-profile.js';

Database

Lets create a posts table:

1
  supabase migration new posts

And then write the sql:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
create table posts (
  id bigint generated by default as identity primary key,
  title text,
  body text,
  user_id uuid references auth.users,
  created_at timestamp with time zone default now(),
  updated_at timestamp with time zone default now()
);

-- 2. Enable RLS
alter table posts enable row level security;

-- select policy
create policy "Posts are visible to everyone."
on posts for select
to anon, authenticated -- the Postgres Role (recommended)
using ( true ); -- the actual Policy

-- insert policy
create policy "Users can create a post."
on posts for insert
to authenticated
with check ( auth.uid() is not null );

Go to the local table view to see the before, and then run

1
  supabase migration up
Connecting to local database...
Applying migration 20240902234248_posts.sql...
Local database is up to date.

To see the after

createPost and getPosts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  // db.js continued
  export async function getPosts() {
    const { data, error } = await supabase
      .from("posts")
      .select("*")
      .order("created_at", { ascending: true });
      
      if (error) {
          console.log( error );
      }

      return {data, error};
  }

  export async function createPost(title, body) {
      const { data, error } = await supabase.from("posts").insert({ title, body });

      if (error) {
          console.log( error );
      }

      return {data, error};
  }

post-form

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  import { createPost } from "./db.js";

  class PostForm extends HTMLElement {
    constructor() {
      super();
    }

    connectedCallback() {
      this.render();
      const form = this.querySelector("form");
      form.addEventListener("submit", this.handleSubmit.bind(this));
    }

    render() {
      this.innerHTML = `
  <form class="input-validation-required">
    <sl-input name="title" label="Title" required></sl-input>
    <br />
    <sl-textarea name="body" label="Body" required></sl-textarea>
    <br /><br />
    <sl-button type="submit" variant="primary">Submit</sl-button>
  </form>`;
    }

    async handleSubmit(event) {
      event.preventDefault();
      const formData = new FormData(event.target);
      const title = formData.get("title");
      const body = formData.get("body");
      await createPost(title, body);
    }
  }

  customElements.define("post-form", PostForm);

And add it to scripts.js:

1
  import './post-form.js';

post-list

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
  import { supabase, getPosts } from "./db.js";

  class PostList extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
      this.posts = [];
    }

    async connectedCallback() {
      this.render();
        const {data,error} = await getPosts();
        this.posts = data;
        if( error ) {
            notify( error.message, "danger" );
            }
      this.channel = supabase
        .channel("schema-db-changes")
        .on(
          "postgres_changes",
          {
            event: "*",
            schema: "public",
          },
          (payload) => {
            console.log(payload);
            if (payload.eventType === "INSERT") {
              this.posts.push(payload.new);
              this.render();
            }
          }
        )
        .subscribe();
      this.render();
    }

    disconnectedCallback() {
      this.channel.unsubscribe();
    }

    render() {
      this.shadowRoot.innerHTML = `
        <style>
          ul {
            list-style: none;
            padding: 0;
          }
          h2 {
            font-size: var(--sl-font-size-large);
          }
          p {
            font-size: var(--sl-font-size-medium);
          }
            time {
            font-size: var(--sl-font-size-small);
          }
        </style>
        <ul class="posts">
        </ul>
      `;

      const ul = this.shadowRoot.querySelector("ul");
      this.posts.forEach((post) => {
        const li = document.createElement("li");
        li.innerHTML = `
  <h2>${post.title}</h2>
  <time>${post.created_at}</time>
  </time>
  <p>${post.body}</p>
  `;
        ul.appendChild(li);
      });
    }
  }

  customElements.define("post-list", PostList);

And add it to scripts.js:

1
  import './post-list.js';

Enable realtime

If you go into the table editor and flip on Realtime on the updates will be broadcast to all active session. Since we subscribed to the database changes, we'll get the updates on all the sites! Very nifty.

Deploying everything

github pages

push this to a repo, and then go to Settings > Pages and set to "deploy from main".

It's that easy.

supabase

Head over to supabase.com and create a project. Make sure you keep a note of your database password!

Then, on your local machine, link the two together:

1
supabase link

Then push the database migrations

1
supabase db push

On the supabase site, go into Settings > API and note the URL and the anon public role.

Change the db.js to have

1
2
3
4
5
6
7
8
9
  let api_url="https://aqghyiuxzwrxqfmcnpmo.supabase.co"
  let anon_key="eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp..."

  if ( window.location.hostname ==  '127.0.0.1' ) {
      api_url="http://127.0.0.1:54321";
      anon_key="eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp..."
      }

  export const supabase = createClient(api_url, anon_key);

Then, in the supabase site go to Authentication > URL Settings and put in the url from above, in my case its https://wschenk.github.io/supabase-auth-test/.

Finally, go to Project Settings > Integrations and connect the github repo to the project. This will run the migrations and other things when you push to the repo so everything is up to date.

Test it out

Go through the steps. You should get a email confirmation email that will link you back to the site.

Make sure that you turn on real time notifications on the table that was created.

Forgot password or password reset isn't implemented by these webcomponents yet, but there's nothing stopping you!

Previously

labnotes

Seperate git for blog writing

i always forget

tags
git

Next

howto

Creating a start page

launch pad for all the things

tags
static_site
start_page