Supabase JS Client: Install and Configure for Web Apps

The Supabase JS Client is your gateway to building full-stack applications with Supabase, letting you interact with your database, authentication, storage, and more directly from your frontend JavaScript.

Let’s get it running. First, you’ll need a Supabase project. If you don’t have one, head over to supabase.com and create a free project. Once that’s done, navigate to your project’s dashboard and find your Project URL and anon public key under Project Settings -> API. You’ll need these.

To install the client, use your preferred package manager. For npm:

npm install @supabase/supabase-js

Or for yarn:

yarn add @supabase/supabase-js

Now, let’s configure it. You’ll want to do this early in your application’s lifecycle, typically in a central configuration file or directly in your main app component.

import { createClient } from '@supabase/supabase-js';

// Replace with your actual Project URL and anon public key
const supabaseUrl = 'https://your-project-ref.supabase.co';
const supabaseAnonKey = 'your-anon-public-key';

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

This supabase object is now your client instance, ready to make requests.

Consider a simple "todos" table in your Supabase database. It might have columns like id (UUID, primary key), task (text), is_complete (boolean, defaults to false), and created_at (timestamp with timezone, defaults to now()).

With the client configured, you can fetch these todos:

async function fetchTodos() {
  const { data, error } = await supabase
    .from('todos')
    .select('*');

  if (error) {
    console.error('Error fetching todos:', error);
    return;
  }

  console.log('Todos:', data);
  // Render these todos in your UI
}

fetchTodos();

The data will be an array of JavaScript objects, each representing a row in your todos table. The error object will contain details if something went wrong.

Inserting a new todo is just as straightforward:

async function addTodo(taskText) {
  const { data, error } = await supabase
    .from('todos')
    .insert([
      { task: taskText, is_complete: false },
    ]);

  if (error) {
    console.error('Error adding todo:', error);
    return;
  }

  console.log('New todo added:', data);
  // You might want to refetch the list or update your UI optimistically
}

addTodo('Learn Supabase JS');

Notice how insert expects an array of objects, even if you’re inserting just one. This is for bulk inserts.

Updating an existing todo:

async function toggleTodoCompletion(todoId, currentStatus) {
  const { data, error } = await supabase
    .from('todos')
    .update({ is_complete: !currentStatus })
    .eq('id', todoId); // .eq() is for equality filter

  if (error) {
    console.error('Error updating todo:', error);
    return;
  }

  console.log('Todo updated:', data);
}

// Assuming you have a todo with id 'some-uuid' and is_complete is false
// toggleTodoCompletion('some-uuid', false);

The .eq('id', todoId) clause is crucial for targeting specific rows. You can chain multiple .eq() calls or use other filter methods like .gt() (greater than), .lt() (less than), .in() (is in an array), etc.

Deleting a todo:

async function deleteTodo(todoId) {
  const { error } = await supabase
    .from('todos')
    .delete()
    .eq('id', todoId);

  if (error) {
    console.error('Error deleting todo:', error);
    return;
  }

  console.log('Todo deleted successfully.');
}

// deleteTodo('some-uuid-to-delete');

The .delete() method, when chained with filters, targets rows for removal.

A powerful feature is real-time subscriptions. You can listen for changes to your data:

const subscription = supabase
  .from('todos')
  .on('*', (payload) => {
    console.log('Change received!', payload);
    // payload.new contains the new row data, payload.old contains the old row data
    // payload.eventType will be 'INSERT', 'UPDATE', or 'DELETE'
  })
  .subscribe();

// To unsubscribe later:
// subscription.unsubscribe();

The on('*', ...) subscribes to all events on the todos table. You can be more specific, e.g., on('INSERT', ...) or on('UPDATE', (payload) => { ... }).

The supabase client instance is designed to be a singleton. You instantiate it once and reuse that instance throughout your application. This ensures consistent configuration and connection management. If you’re using a framework like React, you might create a custom hook or context provider to manage the supabase instance and expose it to your components.

The way Supabase handles row-level security (RLS) policies means that even though you’re sending direct queries from the client, your data remains protected. The anon public key is intentionally limited in scope, and your actual data access is governed by policies defined in your Supabase SQL editor, ensuring only authorized operations succeed.

When you’re dealing with real-time updates, the payload object from the .on() method is your primary source of information about what changed. It contains new and old states of the row, and the eventType. Understanding these fields allows you to precisely update your frontend state without needing to refetch the entire dataset, leading to a more responsive user experience.

The next step is usually integrating authentication, which involves using supabase.auth to handle user sign-ups, logins, and session management.

Want structured learning?

Take the full Supabase course →