Go Back

Supabase Without A Framework

Supabase is well-known for naturally supporting the most common languages and frameworks. However, you can still use Supabase even if you don't want to use their client libraries, or if you use a language that doesn't have a library implemented already (such as Rust 🦀).

At the moment, Supabase provides client libraries in the following languages:

  • JavaScript
  • Flutter
  • Python
  • C#
  • Swift
  • Kotlin

If you're building an app in one of those languages, I'd recommend using their libraries and following their documentation. Supabase can be still be useful even if you're writing an app in another language. Most apps need a combination of CRUD, Auth, and Storage. Supabase has those bases covered.

So, here's how to get started if you're not able to use one of the aforementioned client libraries.

The Basics

Supabase has many features, but for this post I'll give you a quick demo on how to access your database and log in a user. After all, those are by far the most common operations most apps have to handle. I'll likely write another post dealing with Storage and Auth in an equally framework agnostic way, but we'll keep it simple for this one.

There are only three easy steps involved:

1. Get your API key from the Supabase dashboard

There are 2 keys, the anon key, and the service_role key. You can read more about Supabase's API Keys and how policies interact with them if you wish. Find your API keys by going to your project dashboard > Project Settings > API (Under Configuration Heading) and then you can copy your Project API Keys.

Never expose your service_role key to a user!

2. Check the endpoint you want to access

Supabase uses Postgrest to generate REST endpoints for each of your tables automatically, which saves a ton of work over a manual implementation. They also expose Auth endpoints as you will see in the next step.

You can find these endpoints documented at dashboard > API Docs (Left Sidebar).

You will find an example request for each column which is also pretty convenient. At the top right you can find a switch between JavaScript or Bash. You can also choose to show your keys inside the snippets for convenience.

3. Make the request

Which key you need to use depends on the RLS Policies set for the table you are accessing.

You can read more about RLS Policies, but the TLDR is that the service_role key can bypass those policies. I'll be using the service_role key on my demo project for this example.

Here's a Bash example which will SELECT a row from our subs table where the stripe_id column is equal to abc123 :

curl 'https://phcccknqeiahpzgtxfiq.supabase.co/rest/v1/subs?stripe_id=eq.abc123' \
-H "apikey: SUPABASE_KEY" \
-H "Authorization: Bearer SUPABASE_KEY"

This can be translated into any language you like, here is the same request in Rust 🦀:

Rust

use reqwest::header;

#[tokio::main]
async fn main() {
    let mut headers = header::HeaderMap::new();
    headers.insert("apikey", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoY2Nja25xZWlhaHB6Z3R4ZmlxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjMxNjAxMTAsImV4cCI6MjAzODczNjExMH0.BPjOKZ5fKoZO15PYIOF2EkN1riCsJHXrWdYwXOYFdbY".parse().unwrap());
    headers.insert("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoY2Nja25xZWlhaHB6Z3R4ZmlxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjMxNjAxMTAsImV4cCI6MjAzODczNjExMH0.BPjOKZ5fKoZO15PYIOF2EkN1riCsJHXrWdYwXOYFdbY".parse().unwrap());

    let client = reqwest::Client::new();

    let res = client
        .get("https://phcccknqeiahpzgtxfiq.supabase.co/rest/v1/subs?stripe_id=eq.abc123")
        .headers(headers)
        .send()
        .await
        .unwrap()
        .text() // Decodes the response body into text
        .await
        .unwrap();

    println!("{}", res);
}

This returns a response with a JSON body that looks like this:

[{"id":1,"created_at":"2024-08-09T03:55:36.008728+00:00","stripe_id":"abc123"}]

With that, you can parse the JSON of your response and voila, you've got access to your database. No Client Library required!

The same applies to Supabase Auth

Here's an example where we log in a demo user:

curl -X POST 'https://phcccknqeiahpzgtxfiq.supabase.co/auth/v1/token?grant_type=password' \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoY2Nja25xZWlhaHB6Z3R4ZmlxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjMxNjAxMTAsImV4cCI6MjAzODczNjExMH0.BPjOKZ5fKoZO15PYIOF2EkN1riCsJHXrWdYwXOYFdbY" \
-H "Content-Type: application/json" \
-d '{
  "email": "demo@demo.com",
  "password": "qwerqwer"
}'

Again, you can send this same request in your favorite language. Here's another example in rust:

// Log in with Email/Password
use reqwest::header;

#[tokio::main]
async fn main() {
    let mut headers = header::HeaderMap::new();
    headers.insert("apikey", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoY2Nja25xZWlhaHB6Z3R4ZmlxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjMxNjAxMTAsImV4cCI6MjAzODczNjExMH0.BPjOKZ5fKoZO15PYIOF2EkN1riCsJHXrWdYwXOYFdbY".parse().unwrap());
    headers.insert("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBoY2Nja25xZWlhaHB6Z3R4ZmlxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjMxNjAxMTAsImV4cCI6MjAzODczNjExMH0.BPjOKZ5fKoZO15PYIOF2EkN1riCsJHXrWdYwXOYFdbY".parse().unwrap());

    // We will send the demo user's username and password as JSON in our request body
    let body = r#"{"email": "demo@demo.com", "password": "qwerqwer"}"#;

    let client = reqwest::Client::new();

    let res = client
        .post("https://phcccknqeiahpzgtxfiq.supabase.co/auth/v1/token?grant_type=password")
        .headers(headers)
        .body(body)
        .send()
        .await
        .unwrap()
        .text()
        .await
        .unwrap();

    println!("{}", res);
}

If you've sent the correct credentials you will get back a response with a JSON body that looks like this:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsImtpZCI6Ik53SXhTRkZLbjdRVXZaVDQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3BoY2Nja25xZWlhaHB6Z3R4ZmlxLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiIzN2QyODU4OS05Y2YzLTRiMGUtOThkOS0zODk2MmFkZmVhYzMiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzIzMTc4MTExLCJpYXQiOjE3MjMxNzQ1MTEsImVtYWlsIjoiZGVtb0BkZW1vLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnt9LCJyb2xlIjoiYXV0aGVudGljYXRlZCIsImFhbCI6ImFhbDEiLCJhbXIiOlt7Im1ldGhvZCI6InBhc3N3b3JkIiwidGltZXN0YW1wIjoxNzIzMTc0NTExfV0sInNlc3Npb25faWQiOiJiZmRkMGM3MC1iNGFkLTRiN2ItOWU2OC0wMjdiOTNhOWQ2ODIiLCJpc19hbm9ueW1vdXMiOmZhbHNlfQ.Y9SQ_CRSyiT0tQ-n46s1uJzenGLyloMy751Si0hjy7E",
  "token_type": "bearer",
  "expires_in": 3600,
  "expires_at": 1723178111,
  "refresh_token": "LYpYnoEfS-z6ykTgoxWLLw",
  "user": {
    "id": "37d28589-9cf3-4b0e-98d9-38962adfeac3",
    "aud": "authenticated",
    "role": "authenticated",
    "email": "demo@demo.com",
    "email_confirmed_at": "2024-08-09T03:32:42.879506Z",
    "phone": "",
    "confirmed_at": "2024-08-09T03:32:42.879506Z",
    "last_sign_in_at": "2024-08-09T03:35:11.249835701Z",
    "app_metadata": {
      "provider": "email",
      "providers": [
        "email"
      ]
    },
    "user_metadata": {},
    "identities": [
      {
        "identity_id": "a7bd02bf-27f2-435b-8383-4b834ee2465b",
        "id": "37d28589-9cf3-4b0e-98d9-38962adfeac3",
        "user_id": "37d28589-9cf3-4b0e-98d9-38962adfeac3",
        "identity_data": {
          "email": "demo@demo.com",
          "email_verified": false,
          "phone_verified": false,
          "sub": "37d28589-9cf3-4b0e-98d9-38962adfeac3"
        },
        "provider": "email",
        "last_sign_in_at": "2024-08-09T03:32:42.87685Z",
        "created_at": "2024-08-09T03:32:42.876906Z",
        "updated_at": "2024-08-09T03:32:42.876906Z",
        "email": "demo@demo.com"
      }
    ],
    "created_at": "2024-08-09T03:32:42.87307Z",
    "updated_at": "2024-08-09T03:35:11.25505Z",
    "is_anonymous": false
  }
}

You may also have noticed a few key pieces of data in the response object. It contained an access_token as well as a refresh_token. These tokens can be used to manage user sessions, which you can implement in whatever way is most appropriate for your application.

Batteries Included, One Limitation

All of the common features you'd expect are there. You have filtering, pagination (ranges), multi-insertion, and upsert in the box. However, there is one limitation, Supabase's Realtime Streams are currently only supported by their client libraries. For most users this isn't an issue, but it's worth being aware of before jumping in.

Overall, Supabase is a very complete platform. I sincerely hope that they will continue to expand their client library selection to include languages such as Rust and Go, which will make it even more universal. Thankfully, until then we can still make it work well if we put in a modest effort.

Open Source & Community

I have to say before I go on, this post was not sponsored by Supabase. I simply like the tool, the team who built it, and the community that has gathered around the project.

With that, there's a few things I'd like to plug because I believe it's worth your time to look at if you're interested in Supabase and I can personally vouch for the people involved.

  1. A really cool dude named David Lorenz, aka @activenode wrote a book that teaches everything you would ever want to know about Supabase. He also does consulting to help people get the most out of the platform. Check out his book.
  2. The Supabase Discord has a lot of helpful people who answer every question you've ever thought of and more. Join, ask questions, and meet other people who build stuff.
  3. The Build In Public 3 community on X is a heavily moderated community that makes sure the conversation stays helpful and positive. If you are into startups or the indie-hacking scene, come join us.