How I Structure a React and Supabase Project Before Writing the First Feature

 

I like seeing a project work quickly. But I also want to find my own code when I come back to it.

Before I build the first feature in React and Supabase, I make a few small decisions: where the client lives, where database calls go and how the app knows whether someone is signed in. I am not trying to build a perfect architecture. I am giving the next change somewhere to go.

Write down the first useful version

I start with what a person needs to do. Maybe that is sign in, create a record and find it again. Maybe the app only needs public pages and an admin who manages the content. The folder structure should follow that work.

I do not add payments, user accounts or a complicated state library just because they appear in a tutorial. Every extra part gives me another thing to maintain.

A starting structure

src/
  components/
  pages/
  features/
  hooks/
  lib/
  services/
  utils/
  App.jsx
  main.jsx

Reusable interface pieces go in components. Screens go in pages. Shared React logic can go in hooks. I use lib for external library setup and services for reusable backend calls.

When a feature gets bigger, features lets me keep its related code together. I do not need every folder on day one. If a folder has no job yet, it can wait.

Keep one Supabase client

For a Vite browser app, these are example values for .env.local:

VITE_SUPABASE_URL=your-project-url
VITE_SUPABASE_PUBLISHABLE_KEY=your-publishable-key

Then I initialise the client in one file:

// src/lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabasePublishableKey =
  import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY

export const supabase = createClient(
  supabaseUrl,
  supabasePublishableKey
)

The publishable key is intended for client use. Vite variables used in browser code are visible in the built app; they are not a place to hide secrets. Service-role and secret keys belong in a trusted server environment. RLS policies still have to protect the data.

I also set the required values in the hosting environment. A file on my laptop does not automatically configure a deployment.

Give repeated queries one home

A query inside a small component is not automatically wrong. But once I repeat an operation across screens, I prefer a focused function:

// src/services/profileService.js
import { supabase } from '../lib/supabaseClient'

export async function getProfile(userId) {
  return supabase
    .from('profiles')
    .select('*')
    .eq('id', userId)
    .single()
}

This example assumes profiles.id matches the auth user ID. A different schema needs a different filter. .single() also expects one row, so the caller needs to handle the error if the profile does not exist.

The service keeps the query in one place. The component handles loading, the result, an empty state where appropriate, and a useful error message.

Make authentication consistent

I want one place to manage the session loading state and respond to auth changes. The rest of the app should not invent a separate answer on every page.

Protected routes help the interface behave properly, but they do not secure a database table. Before real data goes in, I decide who can read, insert, update and delete each kind of record. Then I test those rules with more than one user.

Deploy a small version early

A basic deployment gives me a chance to check environment values, redirects, route refreshes and authentication outside localhost. I would rather discover those assumptions while the project is small.

The structure is useful if it makes the next change easier. Can I find the query? Can I change a screen without touching unrelated files? Can I understand it after a few days away? Those are better checks than how impressive the folder tree looks.

For the setup details, I use the React quickstart and Supabase Auth with React.

Post a Comment

Previous Post Next Post

Contact Form