# Modifier Methods

Learn how to use modifier methods to refine and control query results in the Roblox Supabase client.

Modifier methods must be used AFTER Filter methods.

Modifier methods are available in the [`SelectModifierStage`](https://roblox-supabase.vercel.app/docs/types#selectmodifierstage) and [`RPCModifierStage`](https://roblox-supabase.vercel.app/docs/types#rpcmodifierstage). These methods allow you to control the ordering, limiting, and formatting of your query results. Modifiers are divided into two categories: **non-final modifiers** that remain chainable within the modifier stage, and **finalizers** that collapse the query chain to `FinalStage`.

## Non-Final Modifiers

Non-final modifiers can be chained together in any order and keep you in the modifier stage for further chaining.

### `order(column: string, options: OrderOptions?)`

Prop

Type

`order?`function(column: string, options: OrderOptions?)

The `order` method sorts query results based on one or more columns. This is a non-final modifier that returns [`ModifierStage`](https://roblox-supabase.vercel.app/docs/types#modifierstage) for further chaining.

    -- Order players by level descending
    local topPlayers, error = client
        :from("players")
        :select("*")
        :order("level", { ascending = false })
        :execute()

    -- Order by multiple columns
    local orderedResults, error = client
        :from("players")
        :select("*")
        :order("status, level.desc, username.asc")
        :execute()

    -- Order with nulls first
    local withNullsFirst, error = client
        :from("players")
        :select("*")
        :order("last_login", { ascending = true, nullsFirst = true })
        :execute()

You can also pass [options](https://roblox-supabase.vercel.app/docs/types#orderoptions) to define how your `order` modifier should behave.

### `limit(count: number, options: LimitOptions?)`

Prop

Type

`limit?`function(count: number, options: LimitOptions?)

The `limit` method restricts the number of rows returned by a query. This is a non-final modifier that returns `ModifierStage` for further chaining.

    -- Get only 10 players
    local limitedPlayers, error = client
        :from("players")
        :select("*")
        :limit(10)
        :execute()

    -- Get top 5 high-level players
    local topFive, error = client
        :from("players")
        :select("*")
        :order("level", { ascending = false })
        :limit(5)
        :execute()

    -- Limit with foreign table
    local withForeignLimit, error = client
        :from("players")
        :select("*, inventory(*)")
        :limit(5, { foreignTable = "inventory" })
        :execute()

You can also pass [options](https://roblox-supabase.vercel.app/docs/types#limitoptions) to define how your `limit` modifier should behave.

### `range(from: number, to: number, options: RangeOptions?)`

Prop

Type

`range?`function(from: number, to: number, options: RangeOptions?)

The `range` method retrieves a specific subset of rows, useful for pagination. This is a non-final modifier that returns `ModifierStage` for further chaining.

    -- Get rows 0-9 (first page of 10 items)
    local firstPage, error = client
        :from("players")
        :select("*")
        :range(0, 9)
        :execute()

    -- Get rows 10-19 (second page)
    local secondPage, error = client
        :from("players")
        :select("*")
        :order("created_at")
        :range(10, 19)
        :execute()

    -- Range with foreign table
    local paginatedWithRelations, error = client
        :from("players")
        :select("*, achievements(*)")
        :range(0, 4, { foreignTable = "achievements" })
        :execute()

You can also pass [options](https://roblox-supabase.vercel.app/docs/types#rangeoptions) to define how your `range` modifier should behave.

## Finalizers

Finalizers collapse the query chain to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage), after which only execution methods (`execute()`, `executeTyped()`) are available.

### `single()`

Prop

Type

`single?`function()

The `single` method expects the query to return exactly one row. If zero or multiple rows are found, it returns an error. This is a finalizer that collapses to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage).

    -- Get exactly one player by ID (will error if not found)
    local player, error = client
        :from("players")
        :select("*")
        :eq("id", 123)
        :single()
        :execute()

    if error then
        if error:find("0 rows") then
            warn("Player not found")
        elseif error:find("more than 1 row") then
            warn("Multiple players found with same ID")
        end
    end

    -- Get single player by unique username
    local uniquePlayer, error = client
        :from("players")
        :select("*")
        :eq("username", "john_doe")
        :single()
        :execute()

### `maybeSingle()`

Prop

Type

`maybeSingle?`function()

The `maybeSingle` method returns one row if found, or nil if no rows are found. It returns an error only if multiple rows are found. This is a finalizer that collapses to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage).

    -- Get player by ID, returns nil if not found
    local player, error = client
        :from("players")
        :select("*")
        :eq("id", 999)
        :maybeSingle()
        :execute()

    if error then
        warn("Error:", error)
    elseif not player then
        print("Player not found")
    else
        print("Found player:", player.username)
    end

    -- Safe user lookup
    local function findUser(email)
        local user, err = client
            :from("users")
            :select("*")
            :eq("email", email)
            :maybeSingle()
            :execute()

        if err then
            warn("Lookup error:", err)
            return nil
        end

        return user
    end

### `csv()`

Prop

Type

`csv?`function()

The `csv` method requests the query results in CSV format. This is a finalizer that collapses to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage).

    -- Get data as CSV
    local csvData, error = client
        :from("players")
        :select("id,username,level,created_at")
        :csv()
        :execute()

    if not error then
        -- csvData is a string in CSV format
        print("CSV Data:")
        print(csvData)

        -- You can parse it or save it to a file
        local lines = csvData:split("\n")
        for _, line in ipairs(lines) do
            print("Line:", line)
        end
    end

    -- Export filtered data as CSV
    local exportData, error = client
        :from("transactions")
        :select("id,amount,type,created_at")
        :gte("created_at", "2024-01-01")
        :lte("created_at", "2024-01-31")
        :order("created_at")
        :csv()
        :execute()

### `returns(representation: "minimal" | "representation")`

Prop

Type

`returns?`function(representation: "minimal" | "representation")

The `returns` method specifies how much information should be returned from mutation operations (INSERT, UPDATE, DELETE, UPSERT). This is a finalizer that collapses to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage).

    -- Insert with minimal return (just success status)
    local success, error = client
        :from("players")
        :insert({
            username = "new_player",
            level = 1
        })
        :returns("minimal")
        :execute()

    if success then
        print("Insert successful")
    end

    -- Update with full data return
    local updatedData, error = client
        :from("players")
        :update({ level = 10 })
        :eq("id", 123)
        :returns("representation")
        :execute()

    if not error then
        print("Updated player:", updatedData[1].username)
    end

    -- Delete with minimal return
    local deleteSuccess, error = client
        :from("inactive_players")
        :delete()
        :lt("last_login", "2023-01-01")
        :returns("minimal")
        :execute()

### `overrideTypes(schema: table)`

Prop

Type

`overrideTypes?`function(schema: table)

The `overrideTypes` method allows you to provide a custom type validation schema for runtime type checking. This is a finalizer that collapses to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage).

    -- Define custom type schema
    local playerSchema = {
        id = "number",
        username = "string",
        level = "number",
        inventory = {
            type = "table",
            schema = {
                item_id = "number",
                quantity = "number"

    -- Use custom schema for validation
    local players, error = client
        :from("players")
        :select("*, inventory(*)")
        :overrideTypes(playerSchema)
        :execute()

    if error then
        warn("Type validation error:", error)
    end

    -- Schema for nested data
    local complexSchema = {
        id = "number",
        name = "string",
        stats = {
            type = "table",
            schema = {
                health = "number",
                mana = "number",
                stamina = "number"
        },
        tags = { type = "table", elementType = "string" }

    local data, error = client
        :from("characters")
        :select("*")
        :overrideTypes(complexSchema)
        :execute()

### `explain(options: ExplainOptions?)`

Prop

Type

`explain?`function(options: ExplainOptions?)

The `explain` method retrieves the PostgreSQL query execution plan, useful for performance analysis and optimization. This is a finalizer that collapses to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage).

    -- Get basic query plan
    local plan, error = client
        :from("players")
        :select("*")
        :eq("level", 10)
        :explain()
        :execute()

    if not error then
        print("Query Plan:")
        print(plan)
    end

    -- Get detailed analysis with timing
    local detailedPlan, error = client
        :from("players")
        :select("*")
        :join("inventory", "players.id = inventory.player_id")
        :eq("players.level", 50)
        :explain({
            analyze = true,
            verbose = true,
            format = "json"
        })
        :execute()

    if not error then
        -- Parse JSON plan for analysis
        local planData = game:GetService("HttpService"):JSONDecode(detailedPlan)
        print("Total cost:", planData[1].Plan["Total Cost"])
    end

You can also pass [options](https://roblox-supabase.vercel.app/docs/types#explainoptions) to define how your `explain` modifier should behave.

## Chaining Modifiers

Modifiers can be chained together in flexible ways. Non-final modifiers can be combined in any order, **and finalizers must come last**.

### Correct Usage Patterns

    -- Chain non-final modifiers
    local results, error = client
        :from("players")
        :select("*")
        :order("level", { ascending = false })
        :limit(10)
        :range(0, 9)
        :execute()

    -- Mix filters and modifiers
    local filtered, error = client
        :from("players")
        :select("*")
        :eq("status", "active")
        :gte("level", 20)
        :order("level")
        :limit(5)
        :execute()

    -- Finalizer ends the chain
    local singleResult, error = client
        :from("players")
        :select("*")
        :eq("id", 123)
        :single()  -- Finalizer: no more chaining after this
        :execute()

### Incorrect Usage Patterns

    -- ERROR: Cannot chain after finalizer
    local errorResult = client
        :from("players")
        :select("*")
        :single()  -- Finalizer
        :limit(10) -- ERROR: Cannot chain after finalizer
        :execute()

    -- ERROR: Finalizer in wrong position
    local errorResult = client
        :from("players")
        :select("*")
        :single()  -- Finalizer too early
        :eq("id", 123)  -- ERROR: Cannot filter after finalizer
        :execute()

## Stage Transition Behavior

Understanding how modifiers transition between stages is crucial for correct usage:

  1. **From Filter Stage to Modifier Stage:**

     * `order()`, `limit()`, `range()` transition from [`SelectFilterStage`](https://roblox-supabase.vercel.app/docs/types#selectfilterstage)/[`RPCFilterStage`](https://roblox-supabase.vercel.app/docs/types#rpcfilterstage) to [`SelectModifierStage`](https://roblox-supabase.vercel.app/docs/types#selectmodifierstage)/[`RPCModifierStage`](https://roblox-supabase.vercel.app/docs/types#rpcmodifierstage)
  2. **Within Modifier Stage:**

     * `order()`, `limit()`, `range()` remain in modifier stage (chainable)
     * Finalizers collapse to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage)
  3. **From Modifier Stage to Final Stage:**

     * `single()`, `maybeSingle()`, `csv()`, `returns()`, `overrideTypes()`, `explain()` collapse to [`FinalStage`](https://roblox-supabase.vercel.app/docs/types#finalstage)

## Error Handling with Modifiers

Different modifiers can produce different types of errors:

    -- single() errors
    local result, err = client
        :from("players")
        :select("*")
        :eq("id", 9999)
        :single()  -- Will error: "0 rows returned"
        :execute()

    -- maybeSingle() errors
    local result, err = client
        :from("players")
        :select("*")
        :eq("level", 1)  -- Multiple level 1 players
        :maybeSingle()   -- Will error: "more than 1 row"
        :execute()

    -- range() errors
    local result, err = client
        :from("players")
        :select("*")
        :range(-1, 10)  -- Invalid range
        :execute()

    -- Always check errors
    if err then
        if err:find("0 rows") then
            -- Handle no results
        elseif err:find("more than 1 row") then
            -- Handle multiple results
        elseif err:find("range") then
            -- Handle range error
        else
            -- Handle other errors
            warn("Query error:", err)
        end
    end

[Filter MethodsLearn how to use filter methods to refine your queries in the Roblox Supabase client.](https://roblox-supabase.vercel.app/docs/FiltersAndModifiers/filters)[Edge FunctionsInvoke serverless functions on Supabase Edge Runtime.](https://roblox-supabase.vercel.app/docs/EdgeFunctions/edge-functions)


# Contribute

Contribution guide for those who would like to support the project by adding new functionalities.

There's a few steps I'd like y'all to know about in order to keep things organized.

## Cloning the repository

If you're familiar with Git & Github, this must be a no brainer. Simply enter these commands in your workspace:

    git clone https://github.com/chteau/Roblox-Supabase.git
    cd ./Roblox-Supabase

Simply create a new branch for your changes and create a pull request once done.

## Creating a pull request

When creating a pull request, you're more than invited to use this template to make me review your changes faster:

    ## Title

    A title to your pull request

    ## Description

    A clear and concise description for the pull request

    - A reference to a related issue in the repo
    - Summary of changes
    - Reasoning
    - Additional context

    ## Documentation

    Add any notes that would help me documenting the new features / changes on the docs. Don't worry about the way you word things, just a few words and a code snippets would do the job.

    ## Contributor License Agreement

    I give Cheeteau, LLC permission to license my contributions on any terms they like. I am giving them this license in order to make it possible for them to accept my contributions into their project.

    **_As far as the law allows, my contributions come as is, without any warranty or condition, and I will not be liable to anyone for any damages related to this software or this license, under any kind of legal claim._**

Before creating a pull request make sure:

  * Filled the markdown above for the title, description and documentation.
  * Added in-code documentation (luadocs comments)
  * Wrote tests for new components / features (in the Unit Test files)

Once you verified all of that, feel free to just continue working on the project or use it! :D

## Executing Unit Tests

To run the unit tests, you need to create the `unittests` table in your Supabase database. Execute the following SQL in your Supabase SQL editor:

    create table public.unittests (
        id serial not null,
        uuid uuid not null default gen_random_uuid (),
        email text null,
        name text null,
        description text null,
        full_text_content text null,
        age integer null,
        price numeric(10, 2) null,
        rating real null,
        score double precision null,
        big_number bigint null,
        is_active boolean null default true,
        is_verified boolean null default false,
        created_at timestamp with time zone null default CURRENT_TIMESTAMP,
        updated_at timestamp with time zone null default CURRENT_TIMESTAMP,
        date_of_birth date null,
        time_of_day time without time zone null,
        tags text[] null default array[]::text[],
        status character varying(20) null default 'pending'::character varying,
        category character varying(50) null,
        valid_daterange daterange null,
        valid_int8range int8range null,
        valid_numrange numrange null,
        valid_tsrange tsrange null,
        metadata jsonb null default '{}'::jsonb,
        preferences jsonb null default '{}'::jsonb,
        ip_address inet null,
        mac_address macaddr null,
        point_coord point null,
        phone_number text null,
        website_url text null,
        external_id text null,
        search_vector tsvector GENERATED ALWAYS as (
            to_tsvector(
            'english'::regconfig,
            (
                (
                (
                    (COALESCE(name, ''::text) || ' '::text) || COALESCE(description, ''::text)
                ) || ' '::text
                ) || COALESCE(full_text_content, ''::text)
            )
            )
        ) STORED null
    ) TABLESPACE pg_default;

### Test Data Insertion

After creating the table, insert the test data using the following SQL:

    INSERT INTO
      public.unittests (
        "id",
        "uuid",
        "email",
        "name",
        "description",
        "full_text_content",
        "age",
        "price",
        "rating",
        "score",
        "big_number",
        "is_active",
        "is_verified",
        "created_at",
        "updated_at",
        "date_of_birth",
        "time_of_day",
        "tags",
        "status",
        "category",
        "valid_daterange",
        "valid_int8range",
        "valid_numrange",
        "valid_tsrange",
        "metadata",
        "preferences",
        "ip_address",
        "mac_address",
        "point_coord",
        "phone_number",
        "website_url",
        "external_id",
        "search_vector"
      )
    VALUES
      (
        '1',
        '4fa11b25-811c-4c5c-850a-f47f80729c71',
        'john.doe@example.com',
        'John Doe',
        'A software developer with 5 years of experience.',
        'John Doe is a software developer specializing in backend systems and database design. He enjoys working with distributed systems and microservices architecture.',
        '30',
        '99.99',
        '4.5',
        '85.5',
        '1000000',
        'true',
        'true',
        '2025-12-09 14:12:04.316978+00',
        '2025-12-09 16:13:30.929152+00',
        '1993-12-05',
        '09:30:00',
        ARRAY["developer", "backend", "postgresql"],
        'active',
        'technology',
        '[2024-01-01,2024-12-31)',
        '[1,1001)',
        '[0.0,100.0]',
        '["2024-01-01 00:00:00","2024-12-31 23:59:59")',
        '{"level":"senior","skills":["python","sql","docker"],"department":"engineering"}',
        '{"theme":"dark","language":"en","notifications":true}',
        '192.168.1.1',
        '08:00:27:00:00:01',
        '(40.7128,-74.006)',
        '+1-555-123-4567',
        'https://johndoe.dev',
        'ext_001',
        '' 5 ':7 ' architectur ':32 ' backend ':19 ' databas ':22 ' design ':23 ' develop ':5,16 ' distribut ':28 ' doe ':2,12 ' enjoy ':25 ' experi ':10 ' john ':1,11 ' microservic ':31 ' softwar ':4,15 ' special ':17 ' SYSTEM ':20,29 ' WORK ':26 ' YEAR ':8'
      ),
      (
        '2',
        '7f226552-6b74-49d5-8be7-818de11a4347',
        'jane.smith@example.com',
        'Jane Smith',
        'Product manager with focus on user experience.',
        'Jane Smith leads product development teams to create user-friendly applications. She focuses on agile methodologies and user-centered design principles.',
        '28',
        '149.99',
        '4.7',
        '92.3',
        '2000000',
        'true',
        'false',
        '2025-12-09 14:17:01.725774+00',
        '2025-12-09 16:13:39.116626+00',
        '1995-02-08',
        '14:15:00',
        ARRAY["manager", "product", "ux"],
        'pending',
        'management',
        '[2024-02-01,2024-11-30)',
        '[1001,2001)',
        '[100.1,200.0]',
        '["2024-02-01 00:00:00","2024-11-30 23:59:59")',
        '{"level":"mid","skills":["agile","research","analytics"],"department":"product"}',
        '{"theme":"light","language":"fr","notifications":false}',
        '10.0.0.1',
        '08:00:27:00:00:02',
        '(51.5074,-0.1278)',
        '+44-20-1234-5678',
        'https://janesmith.com',
        'ext_002',
        '' agil ':25 ' applic ':21 ' center ':30 ' creat ':17 ' design ':31 ' develop ':14 ' experi ':9 ' focus ':6,23 ' friend ':20 ' jane ':1,10 ' lead ':12 ' manag ':4 ' methodolog ':26 ' principl ':32 ' product ':3,13 ' smith ':2,11 ' team ':15 ' USER ':8,19,29 ' USER - cent ':28 ' USER - friend ':18'
      ),
      (
        '3',
        '4a27a94d-49c7-4a89-8ae3-6e596de16dd4',
        'bob.johnson@example.com',
        'Bob Johnson',
        'Data scientist working with machine learning.',
        'Bob Johnson applies statistical models and machine learning algorithms to extract insights from large datasets. He specializes in predictive analytics and data visualization.',
        '35',
        '79.99',
        '4.2',
        '78.9',
        '1500000',
        'false',
        'true',
        '2025-12-09 14:19:30.59008+00',
        '2025-12-09 16:13:49.664087+00',
        '1988-12-10',
        '16:45:00',
        ARRAY["data", "ml", "analytics"],
        'inactive',
        'data-science',
        '[2023-01-01,2023-12-31)',
        '[2001,3001)',
        '[200.1,300.0]',
        '["2023-01-01 00:00:00","2023-12-31 23:59:59")',
        '{"level":"senior","skills":["python","r","tensorflow"],"department":"research"}',
        '{"theme":"dark","language":"en","notifications":true}',
        '172.16.0.1',
        '08:00:27:00:00:03',
        '(35.6762,139.6503)',
        '+81-3-1234-5678',
        'https://bobjohnson.ai',
        'ext_003',
        '' algorithm ':17 ' analyt ':28 ' appli ':11 ' bob ':1,9 ' DATA ':3,30 ' dataset ':23 ' EXTRACT ':19 ' insight ':20 ' johnson ':2,10 ' larg ':22 ' learn ':8,16 ' machin ':7,15 ' model ':13 ' predict ':27 ' scientist ':4 ' special ':25 ' statist ':12 ' visual ':31 ' WORK ':5'
      ),
      (
        '4',
        'ed3d2af7-69d5-467f-a79c-9a9a07a9d4f5',
        'alice.williams@example.com',
        'Alice Williams',
        NULL,
        'Alice Williams is a UX designer with a passion for accessibility and inclusive design.',
        '25',
        NULL,
        '3.9',
        NULL,
        '500000',
        'true',
        'false',
        '2025-12-09 14:21:17.059558+00',
        '2025-12-09 16:13:59.228304+00',
        '1998-12-03',
        NULL,
        ARRAY["design", "ux", "accessibility"],
        'pending',
        'design',
        NULL,
        NULL,
        NULL,
        NULL,
        '{"skills":["figma","sketch","adobe-xd"],"department":"design"}',
        '{}',
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        'ext_004',
        '' ACCESS ':13 ' alic ':1,3 ' design ':8,16 ' inclus ':15 ' passion ':11 ' ux ':7 ' william ':2,4'
      ),
      (
        '5',
        '88427365-1625-4282-b279-4b93468430ce',
        'search.test@example.com',
        'Search Test User',
        'This record contains specific keywords for text search testing.',
        'PostgreSQL is a powerful, open source object-relational database system. It has more than 15 years of active development and a proven architecture that has earned it a strong reputation for reliability, data integrity, and correctness. This is a test for full text search capabilities including phrases and websearch queries.',
        '42',
        '299.99',
        '4.9',
        '99.9',
        '9999999',
        'true',
        'true',
        '2025-12-09 14:24:06.245535+00',
        '2025-12-09 16:14:30.766485+00',
        '1981-01-01',
        '12:00:00',
        ARRAY[
          "search",
          "test",
          "fulltext",
          "postgresql",
          "database"
        ],
        'active',
        'search-test',
        '[2024-01-01,2024-12-31)',
        '[999999,10000001)',
        '[999.9,1000.0]',
        '["2024-01-01 00:00:00","2024-12-31 23:59:59")',
        '{"keywords":["postgresql","database","search","fulltext"],"importance":"high"}',
        '{"search_enabled":true,"results_per_page":50}',
        '8.8.8.8',
        '08:00:27:00:00:06',
        '(0,0)',
        '+1-800-SEARCH',
        'https://postgresql.org',
        'ext_006',
        '' 15 ':28 ' activ ':31 ' architectur ':36 ' capabl ':58 ' contain ':6 ' correct ':49 ' DATA ':46 ' databas ':22 ' develop ':32 ' earn ':39 ' FULL ':55 ' includ ':59 ' integr ':47 ' keyword ':8 ' OBJECT ':20 ' OBJECT - rel ':19 ' OPEN ':17 ' phrase ':60 ' postgresql ':13 ' power ':16 ' proven ':35 ' queri ':63 ' record ':5 ' relat ':21 ' reliabl ':45 ' reput ':43 ' SEARCH ':1,11,57 ' sourc ':18 ' specif ':7 ' strong ':42 ' SYSTEM ':23 ' test ':2,12,53 ' TEXT ':10,56 ' USER ':3 ' websearch ':62 ' YEAR ':29'
      ),
      (
        '6',
        '28e150f9-e612-44f3-be39-bae4684f98d1',
        'array.test@example.com',
        'Array Test',
        'Testing array contains, contained by, and overlap operations.',
        'This record has specific tags for array operation testing.',
        '29',
        '49.99',
        '3.5',
        '65',
        '750000',
        'true',
        'true',
        '2025-12-09 14:26:30.850607+00',
        '2025-12-09 16:14:41.248013+00',
        '1994-03-09',
        '18:30:00',
        ARRAY["a", "b", "c", "d", "e", "f", "g"],
        'pending',
        'array-test',
        '[2024-04-01,2024-09-30)',
        '[5000,6001)',
        '[400.1,500.0]',
        '["2024-04-01 00:00:00","2024-09-30 23:59:59")',
        '{"mixed":["text",123,true],"nested":[1,2,3],"array_test":true}',
        '{"array_field":[1,2,3,4,5]}',
        '10.10.10.10',
        '08:00:27:00:00:07',
        '(45,90)',
        '+1-555-ARRAY',
        'https://array.test',
        'ext_007',
        '' ARRAY ':1,4,17 ' contain ':5,6 ' oper ':10,18 ' overlap ':9 ' record ':12 ' specif ':14 ' tag ':15 ' test ':2,3,19'
      ),
      (
        '7',
        '94922643-8eed-4a4e-bd9a-a99d549b70bf',
        'json.test@example.com',
        'JSON Test',
        'Testing JSON and JSONB operators.',
        'This record has complex JSON structures for testing JSON operators.',
        '33',
        '399.99',
        '4.6',
        '88.8',
        '8888888',
        'false',
        'false',
        '2025-12-09 14:29:44.453187+00',
        '2025-12-09 16:14:48.922569+00',
        '1990-11-11',
        '22:22:00',
        ARRAY["json", "jsonb", "test"],
        'active',
        'json-test',
        '[2024-05-01,2024-08-31)',
        '[7000,8001)',
        '[600.1,700.0]',
        '["2024-05-01 00:00:00","2024-08-31 23:59:59")',
        '{"tags":["json","test"],"user":{"age":33,"name":"JSON Test","active":false},"nested":{"level1":{"level2":{"level3":"deep"}}},"settings":{"theme":"dark","notifications":{"push":false,"email":true}},"permissions":["read","write","execute"]}',
        '{"complex":{"null":null,"array":[1,{"nested":"object"},3],"boolean":true}}',
        '255.255.255.255',
        'ff:ff:ff:ff:ff:ff',
        '(-90,-180)',
        '+1-555-JSON',
        'https://json.test',
        'ext_008',
        '' complex ':11 ' JSON ':1,4,12,16 ' JSONB ':6 ' oper ':7,17 ' record ':9 ' structur ':13 ' test ':2,3,15'
      ),
      (
        '8',
        'a51ab58d-d62c-40b3-9707-6f5f5f000c59',
        'range.test@example.com',
        'Range Test',
        'Testing range operators (range_gt, range_gte, range_lt, range_lte, range_adjacent).',
        'This record has various range types for testing range operators.',
        '45',
        '599.99',
        '2.5',
        '33.3',
        '3333333',
        'true',
        'true',
        '2025-12-09 14:31:40.181437+00',
        '2025-12-09 16:14:57.308863+00',
        '1978-06-06',
        '06:06:06',
        ARRAY["range", "test"],
        'inactive',
        'range-test',
        '[2024-06-01,2024-07-31)',
        '[8001,9001)',
        '[700.1,800.0]',
        '["2024-06-01 06:00:00","2024-07-31 18:00:00")',
        '{"range_tests":{"int":"[8001, 9000]","date":"[2024-06-01, 2024-07-31)"}}',
        '{"range_pref":"[0, 100]"}',
        '127.0.0.1',
        '00:00:00:00:00:00',
        '(90,0)',
        '+1-555-RANGE',
        'https://range.test',
        'ext_009',
        '' adjac ':15 ' gt ':7 ' gte ':9 ' lt ':11 ' lte ':13 ' oper ':5,25 ' rang ':1,4,6,8,10,12,14,20,24 ' record ':17 ' test ':2,3,23 ' TYPE ':21 ' various ':19'
      ),
      (
        '9',
        '58b00ad3-298c-4151-8fb8-78c13f55f8ac',
        'minimal@example.com',
        'Minimal Record',
        NULL,
        NULL,
        '18',
        '0.00',
        '0',
        '0',
        '0',
        'false',
        'false',
        '2025-12-09 14:33:07.34315+00',
        '2025-12-09 16:15:04.507464+00',
        NULL,
        NULL,
        ARRAY[],
        'pending',
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        '{}',
        '{}',
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        'ext_010',
        '' minim ':1 ' record ':2'
      );

### Edge Functions Setup

For Edge Functions testing, deploy these functions to your Supabase project:

**Function**: `hello-world`

    import "jsr:@supabase/functions-js/edge-runtime.d.ts";

    Deno.serve(async (req: Request) => {
        return new Response("Hello, World!", {
            headers: { "Content-Type": "text/plain" },
        });
    });

**Function**: `test-echo`

    import "jsr:@supabase/functions-js/edge-runtime.d.ts";
    interface reqPayload {
        text: string;

    Deno.serve(async (req: Request) => {
        const { text }: reqPayload = await req.json();

        return new Response(
            JSON.stringify({
                text
            }),
            { headers: { 'Content-Type': 'application/json', 'Connection': 'keep-alive' }}
        );
    });

## Run Unit Tests

In a **server script** within Roblox Studio, run the unit tests with the following code:

    local Supabase = require(--[[ path to Supabase module --]])

    local Client = Supabase.createClient({
        url = "<YOUR_SUPABASE_URL>",
        apiKey = "<YOUR_API_OR_ANON_KEY>",
    })

    -- Run all unit tests
    Client._unitTests:runAllTests()

    -- Or run specific test categories
    -- Client._unitTests:runEqualityTest()
    -- Client._unitTests:runMutationTests()
    -- Client._unitTests:runEdgeFunctionsTest()

### Test Categories

The comprehensive test suite includes:

  1. **Filter Tests**: Equality, inequality, comparison, text search, and array operations
  2. **Modifier Tests**: Ordering, limiting, pagination, and response formats
  3. **Mutation Tests**: CRUD operations (insert, update, upsert, delete)
  4. **Edge Functions Tests**: Function invocation with various payloads
  5. **Complex Query Tests**: Multi-condition queries with combined filters

### Expected Outcomes

When tests run successfully, you'll see output similar to:

    SUPABASE> ===========================================
    SUPABASE> Running Supabase Unit Tests...
    SUPABASE> Testing against actual database records
    SUPABASE> ===========================================

    SUPABASE> --- FILTER TESTS ---
    SUPABASE> Running Equality Test...
    SUPABASE> Equality Test Passed.
    ...

    SUPABASE> ===========================================
    SUPABASE> All tests completed successfully!
    SUPABASE> ===========================================

### Troubleshooting

  * **Permission Errors**: Ensure your API key has appropriate permissions for the `unittests` table
  * **Connection Issues**: Verify your Supabase URL and API key are correct
  * **Missing Edge Functions**: Edge Function tests will skip if functions aren't deployed (non-critical)
  * **Data Conflicts**: If tests fail due to existing data, truncate the `unittests` table and re-insert test data


# Selecting a table

Selecting a table from your database using the Supabase client.

# `from(tableName: string)`

The `from()` method is your entry point to database operations in Supabase. It selects a table from your PostgreSQL database and returns a query builder that enables you to construct and execute SQL queries using a fluent, chainable API.

## Parameters

Prop

Type

`tableName`string

## Return Value

The `from()` method returns a [**RootQuery**](https://roblox-supabase.vercel.app/docs/types#rootquery) object, which serves as the foundation for building database queries.

## Usage

Script.luau

    -- Assuming you've defined your Supabase client beforehand

    local data, error = client
        :from("TableName")
        -- Then select the operation you want to execute (i.e. select, insert, update, delete)

[InstallationInstall Roblox Supabase on your Roblox Studio project or using Rojo.](https://roblox-supabase.vercel.app/docs/installation)[Fetch DataFetch data from your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/fetch-data)


# Fetch Data

Fetch data from your PostgreSQL database using the Supabase client.

# `select(columns: string?)`

`select()` can be combined with filters and modifiers.

The `select()` method specifies which columns to retrieve from a table in your PostgreSQL database. This method initiates a SELECT query and returns an object that supports filtering, ordering, limiting, and other query modifiers.

## Method Signature

Prop

Type

`select?`function(columns: string)

## Basic Usage

    -- Select all columns from a table
    local data, err = client
        :from("players")
        :select("*")
        :execute()

    if err then
        warn("Query failed:", err)
    else
        for _, player in ipairs(data) do
            print(player.username, player.level)
        end
    end

## Column Selection Patterns

### Select All Columns

    -- Get all columns from the table
    local allData = client
        :from("players")
        :select("*") -- You can also just type `:select()` to select all columns.
        :execute()

### Select Specific Columns

    -- Get only specific columns
    local partialData = client
        :from("players")
        :select("id, username, level, created_at")
        :execute()

### Select with Related Data

    -- Get player data with their inventory and achievements
    -- Note: Requires proper foreign key relationships in your database
    local playerWithRelations = client
        :from("players")
        :select("*, inventory(*), achievements(*)")
        :eq("id", playerId)
        :single()
        :execute()

## Complete Examples

### Example 1: Basic Data Retrieval

    -- Get all active players with their basic info
    local activePlayers, err = client
        :from("players")
        :select("id, username, level, status")
        :eq("status", "active")
        :execute()

    if err then
        warn("Failed to fetch players:", err)
    else
        for _, player in ipairs(activePlayers) do
            print(string.format("Player: %s (Level: %d)", player.username, player.level))
        end
    end

### Example 2: Using Filters and Modifiers

    -- Get top 10 players by level
    local topPlayers, err = client
        :from("players")
        :select("username, level, experience")
        :gt("level", 10)
        :order("level", { ascending = false })
        :limit(10)
        :execute()

### Example 3: Type-Safe Execution

    -- Define your table type
    type Player = {
        id: number,
        username: string,
        level: number,
        experience: number,
        created_at: string,
        updated_at: string,

    -- Use executeTyped() for runtime type validation
    local players: {Player}, err: string? = client
        :from("players")
        :select("*")
        :executeTyped()

    if err then
        warn("Type validation failed:", err)
    end

### Example 4: Complex Query with Multiple Filters and Modifiers

    -- Find high-level players who joined recently and are active
    local recentHighLevelPlayers, err = client
        :from("players")
        :select("id, username, level, created_at")
        :gte("level", 50)
        :eq("status", "active")
        :gte("created_at", "2024-01-01")
        :order("created_at", { ascending = false })
        :limit(20)
        :execute()

## Method Chaining Order

### Correct Order

    -- Start with from(), then select(), then filters, then modifiers
    local result = client
        :from("players")       -- Step 1: Select table
        :select("*")           -- Step 2: Specify columns
        :eq("status", "active") -- Step 3: Apply filters
        :order("level")        -- Step 4: Apply modifiers
        :limit(10)             -- Step 5: Apply modifiers
        :execute()             -- Step 6: Execute

### Incorrect Order (Will err)

    local result = client
        :from("players")
        :order("level")        -- err: Modifier before select()
        :select("*")           -- This will throw an err
        :execute()

## Error Handling

### Common errors

    -- Column doesn't exist
    local result, err = client
        :from("players")
        :select("non_existent_column")  -- Will return an err
        :execute()

    if err then
        warn("err:", err)  -- "column 'non_existent_column' does not exist"
    end

    -- Table doesn't exist
    local result, err = client
        :from("nonexistent_table")  -- Will return an err
        :select("*")
        :execute()

### Graceful Error Handling

    local function safeSelect(tableName, columns)
        local result, err = client
            :from(tableName)
            :select(columns)
            :execute()

        if err then
            -- Log err and return empty table
            warn(string.format("Select from %s failed: %s", tableName, err))
            return {}
        end

        return result
    end

    -- Usage
    local players = safeSelect("players", "id, username, level")

[Selecting a tableSelecting a table from your database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/select-table)[Insert DataInsert new rows into your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/insert-data)


# Update Data

Update existing rows in your PostgreSQL database using the Supabase client.

# `update(data: {[string]: any}, options: UpdateOptions?)`

`update()` can be combined with filters and returns() modifier.

The `update()` method modifies existing rows in a PostgreSQL table. This method returns an object that allows you to filter which rows to update and specify what data to return from the updated rows.

## Method Signature

Prop

Type

`update?`function(data: {[string]: any}, options: UpdateOptions?)

## Update Options

You can configure the update operation using [`UpdateOptions`](https://roblox-supabase.vercel.app/docs/types#updateoptions).

## Basic Usage

    -- Update a single row by ID
    local result, error = client
        :from("players")
        :update({
            level = 10,
            experience = 5000,
            status = "active"
        })
        :eq("id", 123)
        :execute()

    if error then
        warn("Update failed:", error)
    else
        print("Updated", #result, "row(s)")
    end

## Complete Examples

### Example 1: Update Single Row with Full Return

    -- Update a player and return all updated columns
    local updatedPlayer, error = client
        :from("players")
        :update({
            level = 20,
            experience = 10000,
            last_login = os.date("!%Y-%m-%d %H:%M:%S")
        })
        :eq("id", 123)
        :select("*")
        :single()
        :execute()

    if error then
        warn("Failed to update player:", error)
    else
        print("Player updated. New level:", updatedPlayer.level)
    end

### Example 2: Update Multiple Rows with Conditions

    -- Update all inactive players to active status
    local result, error = client
        :from("players")
        :update({
            status = "active",
            updated_at = os.date("!%Y-%m-%d %H:%M:%S")
        })
        :eq("status", "inactive")
        :gt("last_login", "2024-01-01")
        :execute()

    if error then
        warn("Batch update failed:", error)
    else
        print("Updated", #result, "players to active status")
    end

### Example 3: Minimal Return (No Data)

    -- Update with minimal return (just success/failure)
    local success, error = client
        :from("players")
        :update({
            last_seen = os.date("!%Y-%m-%d %H:%M:%S")
        })
        :eq("id", 123)
        :returns("minimal")
        :execute()

    if error then
        warn("Update failed:", error)
    else
        print("Last seen timestamp updated successfully")
    end

### Example 4: Update with Complex Conditions

    -- Update high-level players who haven't logged in recently
    local result, error = client
        :from("players")
        :update({
            status = "inactive",
            inactive_reason = "inactivity"
        })
        :gt("level", 50)
        :lt("last_login", "2024-01-01")
        :eq("status", "active")
        :execute()

    if error then
        warn("Complex update failed:", error)
    else
        print("Marked", #result, "players as inactive")
    end

### Example 5: Update with Type Validation

    -- Define a type for validation
    type PlayerUpdate = {
        level: number,
        experience: number?,
        status: string?,
        last_login: string?

    -- Update with type validation
    local updateData: PlayerUpdate = {
        level = 15,
        experience = 7500,
        status = "active",
        last_login = os.date("!%Y-%m-%d %H:%M:%S")

    local result, error = client
        :from("players")
        :update(updateData)
        :eq("id", 123)
        :executeTyped()

    if error then
        warn("Type validation failed:", error)
    end

## Method Chaining Order

### Correct Order

    -- Start with from(), then update(), then filters, then allowed modifiers
    local result = client
        :from("players")        -- Step 1: Select table
        :update({               -- Step 2: Update data
            level = 10,
            status = "active"
        })
        :eq("id", 123)          -- Step 3: Apply filters (required!)
        :returns("representation") -- Step 4: Specify return format (optional)
        :execute()              -- Step 5: Execute

### Important: Filters are REQUIRED for update()

    -- Always use filters to target specific rows
    local safeResult = client
        :from("players")
        :update({status = "active"})
        :eq("id", 123)  -- Specific filter
        :execute()

## Error Handling

### Common Update Errors

    -- No rows matched (not necessarily an error, but check your filter)
    local result, err = client
        :from("players")
        :update({level = 10})
        :eq("id", 999999)  -- Non-existent ID
        :execute()

    if #result == 0 then
        print("No rows were updated - check your filter conditions")
    end

    -- Constraint violation
    local result, err = client
        :from("players")
        :update({username = ""})  -- Empty string might violate constraint
        :eq("id", 123)
        :execute()

    if err then
        warn("Constraint error:", err)  -- e.g., "violates check constraint"
    end

### Graceful Error Handling

    local function safeUpdate(tableName, data, filterColumn, filterValue)
        local result, err = client
            :from(tableName)
            :update(data)
            :eq(filterColumn, filterValue)
            :returns("minimal")
            :execute()

        if err then
            -- Check for specific error types
            if string.find(err, "violates check constraint") then
                warn("Invalid data violates constraint")
                return false, "constraint_violation"
            elseif string.find(err, "null value") then
                warn("Cannot set column to null")
                return false, "null_violation"
            else
                warn(string.format("Update on %s failed: %s", tableName, err))
                return false, "unknown_error"
            end
        end

        return true, nil
    end

    -- Usage
    local success, errType = safeUpdate("players",
        {level = 100},
        "id",
        123
    )

[Insert DataInsert new rows into your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/insert-data)[Upsert DataInsert or update rows in your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/upsert-data)


# Insert Data

Insert new rows into your PostgreSQL database using the Supabase client.

# `insert(data: {[string]: any} | {[string]: any}[], options: InsertOptions?)`

The `insert()` method creates new rows in a PostgreSQL table. This method returns an object that allows you to specify what data to return from the inserted rows.

## Method Signature

Prop

Type

`insert?`function(data: {[string]: any} | {[string]: any}[], options: InsertOptions?)

## Insert Options

You can configure the insert operation using [`InsertOptions`](https://roblox-supabase.vercel.app/docs/types#insertoptions).

## Basic Usage

    -- Insert a single row
    local result, err = client
        :from("players")
        :insert({
            username = "NewPlayer",
            level = 1,
            experience = 0,
            status = "active"
        })
        :execute()

    if err then
        warn("Insert failed:", err)
    else
        print("Inserted player ID:", result[1].id)
    end

## Complete Examples

### Example 1: Insert Single Row with Full Return

    -- Insert a player and return all columns
    local newPlayer, err = client
        :from("players")
        :insert({
            username = "DragonSlayer",
            level = 1,
            experience = 0,
            email = "dragon@example.com",
            status = "active"
        })
        :select("*")
        :single()
        :execute()

    if err then
        warn("Failed to insert player:", err)
    else
        print("New player created with ID:", newPlayer.id)
    end

### Example 2: Insert Multiple Rows

    -- Insert multiple players at once
    local players, err = client
        :from("players")
        :insert({
                username = "Warrior",
                level = 1,
                class = "warrior"
            },
                username = "Mage",
                level = 1,
                class = "mage"
            },
                username = "Rogue",
                level = 1,
                class = "rogue"
        })
        :select("id, username, class")
        :execute()

    if err then
        warn("Batch insert failed:", err)
    else
        for _, player in ipairs(players) do
            print(string.format("Created %s (%s) with ID: %d",
                player.username, player.class, player.id))
        end
    end

### Example 3: Minimal Return (No Data)

    -- Insert with minimal return (just success/failure)
    local success, err = client
        :from("game_logs")
        :insert({
            player_id = 123,
            action = "login",
            created_at = os.date("!%Y-%m-%d %H:%M:%S")
        })
        :returns("minimal")
        :execute()

    if err then
        warn("Log insertion failed:", err)
    else
        print("Log entry created successfully")
    end

### Example 4: Insert with Default Values

    -- Insert using database defaults for some columns
    local result, err = client
        :from("players")
        :insert({
            username = "DefaultUser",
            -- level will use default value (e.g., 1)
            -- created_at will use default (CURRENT_TIMESTAMP)
        })
        :select("*")
        :single()
        :execute()

### Example 5: Insert with Type Validation

    -- Define a type for validation
    type PlayerInsert = {
        username: string,
        level: number,
        experience: number?,
        email: string?,
        status: string

    -- Insert with type validation
    local player: PlayerInsert = {
        username = "TypeSafePlayer",
        level = 5,
        experience = 1000,
        status = "active"

    local result, err = client
        :from("players")
        :insert(player)
        :executeTyped()

    if err then
        warn("Type validation failed:", err)
    end

## Method Chaining Order

### Correct Order

    -- Start with from(), then insert(), then modifiers
    local result = client
        :from("players")       -- Step 1: Select table
        :insert({              -- Step 2: Insert data
            username = "TestPlayer",
            level = 1
        })
        :select("*")           -- Step 3: Specify return columns (optional)
        :single()              -- Step 4: Expect single row (optional)
        :execute()             -- Step 5: Execute

### Insert-Specific Modifiers

    -- Insert with different return formats
    local result = client
        :from("players")
        :insert({username = "CSVTest", level = 1})
        :csv()                 -- Request CSV format
        :execute()

## Error Handling

### Common Insert Errors

    -- Missing required column
    local result, err = client
        :from("players")
        :insert({
            -- Forgot required 'username' field
            level = 1
        })
        :execute()

    if err then
        warn("Insert err:", err)  -- "null value in column 'username' violates not-null constraint"
    end

    -- Duplicate unique constraint
    local result, err = client
        :from("players")
        :insert({
            username = "ExistingPlayer",  -- Already exists
            level = 1
        })
        :execute()

    if err then
        warn("Duplicate err:", err)  -- "duplicate key value violates unique constraint"
    end

### Graceful Error Handling

    local function safeInsert(tableName, data)
        local result, err = client
            :from(tableName)
            :insert(data)
            :returns("minimal")
            :execute()

        if err then
            -- Check for specific error types
            if string.find(err, "duplicate key") then
                warn("Duplicate entry detected")
                return false, "duplicate"
            elseif string.find(err, "null value") then
                warn("Missing required field")
                return false, "missing_field"
            else
                warn(string.format("Insert into %s failed: %s", tableName, err))
                return false, "unknown_err"
            end
        end

        return true, nil
    end

    -- Usage
    local success, errType = safeInsert("players", {
        username = "TestPlayer",
        level = 1
    })

[Fetch DataFetch data from your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/fetch-data)[Update DataUpdate existing rows in your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/update-data)


# Edge Functions

Invoke serverless functions on Supabase Edge Runtime.

# Edge Functions Client

The Edge Functions client allows you to invoke serverless functions running on Supabase's global edge network. This is useful for executing backend logic without managing servers.

## `invoke(functionName, payload?, options?)`

Use for JSON responses

This method automatically parses JSON responses and handles common error patterns. It's the recommended way to invoke most Edge Functions.

Prop

Type

`invoke?`function(functionName: string, payload: table?) -> (any, string?)

You can also use some [options](https://roblox-supabase.vercel.app/docs/types#functionsinvokeoptions) in order to invoke your function.

### Basic Invocation

    -- Invoke a function without payload
    local result, err = client.functions:invoke("hello-world")

    if err then
        warn("Function failed:", err)
    else
        print("Response:", result)
    end

    -- Invoke with payload
    local data, err = functions:invoke("process-data", {
        userId = 123,
        action = "update",
        metadata = {
            level = 50,
            items = {"sword", "shield"}
    })

### Handling Different Response Types

    -- JSON response (automatically parsed)
    local jsonResult, err = functions:invoke("get-user-data", { id = 123 })
    -- Returns: { username = "player1", level = 50 } (as table)

    -- Text response (plain text)
    local textResult, err = functions:invoke("generate-token")
    -- Returns: "abc123def456" (as string)

    -- Empty response
    local result, err = functions:invoke("log-activity", { event = "login" })
    -- Returns: nil, nil on success

### Error Handling Examples

    -- Function doesn't exist
    local result, err = functions:invoke("nonexistent-function")
    if err then
        warn(err) -- "Edge Function 'nonexistent-function' failed: Not Found"
    end

    -- Function returns error
    local result, err = functions:invoke("failing-function")
    if err then
        warn(err) -- "Edge Function 'failing-function' failed: Internal Server Error"
    end

    -- Network failure
    local result, err = functions:invoke("some-function")
    if err then
        warn(err) -- "Edge Function 'some-function' failed: Connection failed"
    end

## `invokeRaw(functionName, payload?, options?)`

Use for full control

This method returns the raw HTTP response table, giving you access to status codes, headers, and raw body. Use this when you need to handle non-JSON responses or inspect response metadata.

Prop

Type

`invokeRaw?`function(functionName: string, payload: table?) -> (table, string?)

You can also use some [options](https://roblox-supabase.vercel.app/docs/types#functionsinvokeoptions) in order to invoke your function.

### Raw Response Structure

You can see those on the [Roblox Official Documentation](https://create.roblox.com/docs/reference/engine/classes/HttpService?utm_source=studio&utm_content=hover_tip#RequestAsync):

    local response, err = functions:invokeRaw("get-data")

    if not err then
        print("Success:", response.Success) -- boolean
        print("Status:", response.StatusCode) -- number (200, 404, 500, etc.)
        print("Message:", response.StatusMessage) -- string ("OK", "Not Found", etc.)
        print("Headers:", response.Headers) -- table of header key-value pairs
        print("Body:", response.Body) -- raw response body as string
    end

### Accessing Response Headers

    local response, err = functions:invokeRaw("generate-file")

    if not err and response.Success then
        local contentType = response.Headers["Content-Type"]
        local contentLength = response.Headers["Content-Length"]

        if contentType == "application/pdf" then
            -- Handle PDF data
            print("Received PDF of size:", contentLength)
        end
    end

### Manual JSON Parsing

    local response, err = functions:invokeRaw("get-complex-data")

    if not err and response.Success then
        local ok, parsed = pcall(function()
            return HttpService:JSONDecode(response.Body)
        end)

        if ok then
            print("Parsed data:", parsed)
        else
            warn("Failed to parse JSON:", parsed)
        end
    end

## Error Patterns and Solutions

### Common Errors

    -- 1. Function not deployed
    local result, err = functions:invoke("undeployed-function")
    -- Error: "Edge Function 'undeployed-function' failed: Not Found"

    -- 2. Function execution error
    local result, err = functions:invoke("buggy-function")
    -- Error: "Edge Function 'buggy-function' failed: Internal Server Error"

    -- 3. Network issues
    local result, err = functions:invoke("any-function")
    -- Error: "Edge Function 'any-function' failed: Connection failed"

### Retry Logic Implementation

    local function invokeWithRetry(functionName, payload, maxRetries)
        local lastError

        for attempt = 1, maxRetries do
            local result, err = functions:invoke(functionName, payload)

            if not err then
                return result
            end

            lastError = err

            -- Only retry on network errors, not application errors
            if not string.find(err:lower(), "connection") and
               not string.find(err:lower(), "timeout") then
                break
            end

            -- Exponential backoff
            local waitTime = math.min(2 ^ (attempt - 1), 10)
            print(string.format("Retry %d/%d in %ds...", attempt, maxRetries, waitTime))
            task.wait(waitTime)
        end

        return nil, "Failed after " .. maxRetries .. " retries: " .. (lastError or "unknown error")
    end

    -- Usage
    local data = invokeWithRetry("important-function", { id = 123 }, 3)

## Best Practices

### 1\. Use Appropriate Method

    -- Use invoke() for most cases
    local userData, err = functions:invoke("get-user", { id = 123 })

    -- Use invokeRaw() only when you need:
    -- - Response headers
    -- - Raw response body
    -- - Custom parsing
    -- - Binary data
    local rawResponse, err = functions:invokeRaw("download-file", { fileId = 456 })

### 2\. Handle Timeouts Appropriately

    local function invokeWithTimeout(functionName, payload, timeout)
        local result
        local err

        local thread = coroutine.create(function()
            result, err = functions:invoke(functionName, payload)
        end)

        coroutine.resume(thread)

        local start = os.clock()
        while coroutine.status(thread) ~= "dead" do
            if os.clock() - start > timeout then
                return nil, "Request timeout"
            end
            task.wait()
        end

        return result, err
    end

### 3\. Batch Operations

    local function batchInvoke(requests)
        local responses = {}

        -- Use invokeRaw to avoid unnecessary JSON parsing
        for _, req in ipairs(requests) do
            local response, err = functions:invokeRaw(req.functionName, req.payload)
            table.insert(responses, {
                request = req,
                response = response,
                error = err
            })
        end

        return responses
    end

[Modifier MethodsLearn how to use modifier methods to refine and control query results in the Roblox Supabase client.](https://roblox-supabase.vercel.app/docs/FiltersAndModifiers/modifiers)[TypesComplete reference of all types defined and used by the Roblox Supabase client.](https://roblox-supabase.vercel.app/docs/types)


# Delete Data

Delete rows from your PostgreSQL database using the Supabase client.

# `delete(options: DeleteOptions?)`

`delete()` can be combined with filters and returns() modifier.

The `delete()` method removes rows from a PostgreSQL table. This method returns an object that allows you to filter which rows to delete and specify what data to return from the deleted rows. **Use with caution as deleted data cannot be recovered.**

## Method Signature

Prop

Type

`delete?`function(options: DeleteOptions?)

## Delete Options

You can configure the delete operation using [`DeleteOptions`](https://roblox-supabase.vercel.app/docs/types#deleteoptions)

## Basic Usage

    -- Delete a single row by ID
    local result, err = client
        :from("players")
        :delete()
        :eq("id", 123)
        :execute()

    if err then
        warn("Delete failed:", err)
    else
        print("Deleted", #result, "row(s)")
    end

## Complete Examples

### Example 1: Delete Single Row with Full Return

    -- Delete a player and return the deleted data
    local deletedPlayer, err = client
        :from("players")
        :delete()
        :eq("id", 123)
        :select("*")
        :single()
        :execute()

    if err then
        warn("Failed to delete player:", err)
    else
        print("Deleted player:", deletedPlayer.username, "ID:", deletedPlayer.id)
    end

### Example 2: Delete Multiple Rows with Conditions

    -- Delete all inactive players who haven't logged in for 30 days
    local result, err = client
        :from("players")
        :delete()
        :eq("status", "inactive")
        :lt("last_login", os.date("!%Y-%m-%d", os.time() - 30 * 24 * 60 * 60))
        :execute()

    if err then
        warn("Batch delete failed:", err)
    else
        print("Deleted", #result, "inactive players")
    end

### Example 3: Minimal Return (No Data)

    -- Delete with minimal return (just success/failure)
    local success, err = client
        :from("temporary_data")
        :delete()
        :lt("expires_at", os.date("!%Y-%m-%d %H:%M:%S"))
        :returns("minimal")
        :execute()

    if err then
        warn("Cleanup failed:", err)
    else
        print("Expired data cleaned up successfully")
    end

### Example 4: Delete with Complex Conditions

    -- Delete low-level players who haven't logged in for a year
    local result, err = client
        :from("players")
        :delete()
        :lt("level", 5)
        :lt("last_login", "2023-01-01")
        :eq("status", "inactive")
        :execute()

    if err then
        warn("Complex delete failed:", err)
    else
        print("Cleaned up", #result, "inactive low-level players")
    end

### Example 5: Delete with Type Validation

    -- Define a type for validation
    type DeletedPlayer = {
        id: number,
        username: string,
        level: number,
        deleted_at: string

    -- Delete with type validation on returned data
    local deletedPlayers: {DeletedPlayer}, err = client
        :from("players")
        :delete()
        :eq("status", "test_account")
        :executeTyped()

    if err then
        warn("Type validation failed:", err)
    else
        for _, player in ipairs(deletedPlayers) do
            print("Deleted test account:", player.username)
        end
    end

### Example 6: Delete with OR Conditions

    -- Delete players who are either inactive OR have negative balance
    local result, err = client
        :from("players")
        :delete()
        :or_("status.eq.inactive,balance.lt.0")
        :execute()

    if err then
        warn("OR delete failed:", err)
    else
        print("Deleted", #result, "players with OR conditions")
    end

## Method Chaining Order

### Correct Order

    -- Start with from(), then delete(), then filters, then modifiers
    local result = client
        :from("players")        -- Step 1: Select table
        :delete()               -- Step 2: Initiate delete
        :eq("id", 123)          -- Step 3: Apply filters (required!)
        :returns("representation") -- Step 4: Specify return format (optional)
        :execute()              -- Step 5: Execute

### ⚠ CRITICAL: Filters are REQUIRED for delete()

    -- ⚠ DANGER: This will delete ALL rows in the table!
    local result = client
        :from("players")
        :delete()               -- No filter - deletes EVERY player!
        :execute()

    -- ALWAYS use filters to target specific rows
    local safeResult = client
        :from("players")
        :delete()
        :eq("id", 123)          -- Specific filter
        :execute()

## Common Patterns

### Soft Delete Pattern (Recommended)

    -- Instead of hard delete, mark as deleted (soft delete)
    local result, err = client
        :from("players")
        :update({
            deleted = true,
            deleted_at = os.date("!%Y-%m-%d %H:%M:%S"),
            deleted_by = "system_cleanup"
        })
        :lt("last_login", "2023-01-01")
        :eq("deleted", false)
        :execute()

    -- Later, you can permanently delete soft-deleted records
    local permanentDelete = client
        :from("players")
        :delete()
        :eq("deleted", true)
        :lt("deleted_at", os.date("!%Y-%m-%d", os.time() - 90 * 24 * 60 * 60)) -- 90 days ago
        :execute()

### Delete with Cascade Pattern

    -- Delete player and their related data (if cascade is set up in DB)
    local deletedPlayer, err = client
        :from("players")
        :delete()
        :eq("id", 123)
        :select("*, inventory(*), achievements(*)")
        :single()
        :execute()

    -- Note: This requires ON DELETE CASCADE in your database schema

### Archive Before Delete Pattern

    -- Archive data before deleting
    local function archiveAndDelete(playerId)
        -- First, archive the data
        local playerData = client
            :from("players")
            :select("*")
            :eq("id", playerId)
            :single()
            :execute()

        if playerData then
            -- Insert into archive table
            client
                :from("players_archive")
                :insert({
                    original_id = playerData.id,
                    data = playerData,
                    archived_at = os.date("!%Y-%m-%d %H:%M:%S"),
                    archived_by = "system"
                })
                :execute()

            -- Then delete from main table
            client
                :from("players")
                :delete()
                :eq("id", playerId)
                :returns("minimal")
                :execute()

            return true
        end

        return false
    end

## Error Handling

### Common Delete Errors

    -- Foreign key constraint violation
    local result, err = client
        :from("categories")
        :delete()
        :eq("id", 1)
        :execute()

    if err then
        warn("Delete err:", err)
        -- "update or delete on table 'categories' violates foreign key constraint"
        -- "on table 'products', Key (category_id)=(1) is still referenced from table 'products'"
    end

    -- No rows matched (not necessarily an err, but check your filter)
    local result, err = client
        :from("players")
        :delete()
        :eq("id", 999999)  -- Non-existent ID
        :execute()

    if #result == 0 then
        print("No rows were deleted - check your filter conditions")
    end

### Graceful Error Handling with Transaction Safety

    local function safeDelete(tableName, filterColumn, filterValue)
        -- First, verify the row exists
        local existing = client
            :from(tableName)
            :select("id")
            :eq(filterColumn, filterValue)
            :single()
            :execute()

        if not existing then
            return false, "not_found"
        end

        -- Then perform the delete
        local result, err = client
            :from(tableName)
            :delete()
            :eq(filterColumn, filterValue)
            :returns("minimal")
            :execute()

        if err then
            -- Check for specific err types
            if string.find(err, "violates foreign key constraint") then
                warn("Cannot delete: referenced by other tables")
                return false, "foreign_key_violation"
            elseif string.find(err, "null value") then
                warn("Unexpected null constraint err")
                return false, "constraint_err"
            else
                warn(string.format("Delete from %s failed: %s", tableName, err))
                return false, "unknown_err"
            end
        end

        return true, nil
    end

    -- Usage
    local success, errType = safeDelete("players", "id", 123)

[Upsert DataInsert or update rows in your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/upsert-data)[Call a PostgreSQL FunctionInvoke PostgreSQL functions (stored procedures) using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/call-postgres-function)


# Types

Complete reference of all types defined and used by the Roblox Supabase client.

# Types Reference

This document provides complete type definitions for all objects and interfaces used throughout the Roblox Supabase client. These types enable type-safe database operations and provide full IntelliSense support in Roblox Studio.

## Core Query Types

### `RootQuery`

The entry point for all database operations after selecting a table with `from()`. Provides methods to start SELECT, INSERT, UPDATE, UPSERT, DELETE, and RPC queries.

Prop

Type

`select?`function(columns: string) -> SelectFilterStage

`insert?`function(data: {[string]: any}, options: InsertOptions?) -> InsertQuery

`update?`function(data: {[string]: any}, options: UpdateOptions?) -> UpdateFilterStage

`upsert?`function(data: {[string]: any}, options: InsertOptions?) -> UpsertQuery

`delete?`function(options: DeleteOptions?) -> DeleteFilterStage

`rpc?`function(fnName: string, params: {any}?) -> RPCFilterStage

## Query Stage System

The type system uses a sophisticated stage-based approach with finalizer collapsing. Understanding these stages is key to using the API effectively:

### Stage Flow Overview

  1. **Filter Stage**: Initial stage for SELECT, UPDATE, DELETE, and RPC queries where filter methods are chainable
  2. **Modifier Stage**: For SELECT/RPC after using non-final modifiers (`order`, `limit`, `range`)
  3. **Mutation Stages**: Special stages for INSERT, UPDATE, DELETE operations
  4. **Final Stage**: Collapsed stage with only execution methods after using finalizers

### Key Concepts

  * **Chainable Methods**: Return the same stage type for further chaining
  * **Transitioning Methods**: Move to a different stage type
  * **Finalizers**: Collapse the chain to `FinalStage` with only `execute()`/`executeTyped()`
  * **Non-Final Modifiers**: Remain in modifier stage for further chaining

## SELECT Query Types

### `SelectFilterStage`

The initial stage for SELECT queries. Filter methods are chainable, while modifiers transition to `SelectModifierStage` or collapse to `FinalStage`.

Prop

Type

`FilterMethods?`FilterStage<SelectFilterStage>

`order?`function(column: string, options: OrderOptions?) -> SelectModifierStage

`limit?`function(count: number, options: LimitOptions?) -> SelectModifierStage

`range?`function(from: number, to: number, options: RangeOptions?) -> SelectModifierStage

`single?`function() -> FinalStage

`maybeSingle?`function() -> FinalStage

`csv?`function() -> FinalStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`overrideTypes?`function(schema: table) -> FinalStage

`explain?`function(options: ExplainOptions?) -> FinalStage

`BaseExec?`BaseExec

### `SelectModifierStage`

The stage after applying non-final modifiers (`order`, `limit`, `range`) in a SELECT query.

Prop

Type

`order?`function(column: string, options: OrderOptions?) -> SelectModifierStage

`limit?`function(count: number, options: LimitOptions?) -> SelectModifierStage

`range?`function(from: number, to: number, options: RangeOptions?) -> SelectModifierStage

`single?`function() -> FinalStage

`maybeSingle?`function() -> FinalStage

`csv?`function() -> FinalStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`overrideTypes?`function(schema: table) -> FinalStage

`explain?`function(options: ExplainOptions?) -> FinalStage

`BaseExec?`BaseExec

## Mutation Query Types (INSERT/UPSERT)

### `InsertQuery` / `UpsertQuery`

Initial stage for INSERT and UPSERT operations. These start in `MutationInitialStage` with no filter support.

Prop

Type

`select?`function(columns: string) -> MutationSelectStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`csv?`function() -> FinalStage

`BaseExec?`BaseExec

## Mutation Query Types (UPDATE/DELETE)

### `UpdateFilterStage`

Initial stage for UPDATE operations with filter support.

Prop

Type

`FilterMethods?`FilterStage<UpdateFilterStage>

`select?`function(columns: string) -> MutationSelectStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`csv?`function() -> FinalStage

`BaseExec?`BaseExec

### `DeleteFilterStage`

Initial stage for DELETE operations with filter support.

Prop

Type

`FilterMethods?`FilterStage<DeleteFilterStage>

`select?`function(columns: string) -> MutationSelectStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`csv?`function() -> FinalStage

`BaseExec?`BaseExec

## RPC Query Types

### `RPCFilterStage`

Initial stage for calling PostgreSQL stored procedures with filter support.

Prop

Type

`FilterMethods?`FilterStage<RPCFilterStage>

`order?`function(column: string, options: OrderOptions?) -> RPCModifierStage

`limit?`function(count: number, options: LimitOptions?) -> RPCModifierStage

`range?`function(from: number, to: number, options: RangeOptions?) -> RPCModifierStage

`single?`function() -> FinalStage

`maybeSingle?`function() -> FinalStage

`csv?`function() -> FinalStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`overrideTypes?`function(schema: table) -> FinalStage

`explain?`function(options: ExplainOptions?) -> FinalStage

`get?`function() -> RPCFilterStage

`BaseExec?`BaseExec

### `RPCModifierStage`

The stage after applying non-final modifiers (`order`, `limit`, `range`) in an RPC query.

Prop

Type

`order?`function(column: string, options: OrderOptions?) -> RPCModifierStage

`limit?`function(count: number, options: LimitOptions?) -> RPCModifierStage

`range?`function(from: number, to: number, options: RangeOptions?) -> RPCModifierStage

`single?`function() -> FinalStage

`maybeSingle?`function() -> FinalStage

`csv?`function() -> FinalStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`overrideTypes?`function(schema: table) -> FinalStage

`explain?`function(options: ExplainOptions?) -> FinalStage

`BaseExec?`BaseExec

## Mutation Select Stage

### `MutationSelectStage`

The stage after calling `.select()` on a mutation query (INSERT, UPDATE, DELETE, UPSERT). This stage exposes `single` and `maybeSingle` which are not available in `MutationInitialStage`.

Prop

Type

`single?`function() -> FinalStage

`maybeSingle?`function() -> FinalStage

`returns?`function(representation: "minimal" | "representation") -> FinalStage

`csv?`function() -> FinalStage

`BaseExec?`BaseExec

## Base Types

### `BaseExec`

Base interface containing execution methods common to all query stages.

Prop

Type

`execute?`function() -> (any, string?)

`executeTyped?`function() -> (any, string?)

### `FilterStage<Next>`

Generic filter stage type used internally. All filter methods return the `Next` type parameter, enabling type-safe chaining.

### `ModifierStage`

Modifier stage for SELECT/RPC queries with non-final modifiers (`order`, `limit`, `range`) that remain chainable, and finalizers that collapse to `FinalStage`.

### `MutationInitialStage`

Initial stage for mutation queries (INSERT/UPSERT) before calling `.select()`. Exposes `returns` and `csv` as finalizers, and `.select()` to transition to `MutationSelectStage`.

### `FinalStage`

Final collapsed stage with only execution methods. Reached after calling any finalizer method (`single`, `maybeSingle`, `csv`, `returns`, `overrideTypes`, `explain`).

Prop

Type

`execute?`function() -> (any, string?)

`executeTyped?`function() -> (any, string?)

## Configuration Types

### `InsertOptions`

Configuration options for INSERT and UPSERT operations.

Prop

Type

`returning?`"minimal" | "representation"?

`count?`"exact" | "planned" | "estimated"?

`defaultToNull?`boolean?

`onConflict?`string?

`ignoreDuplicates?`boolean?

`upsert?`boolean?

### `UpdateOptions`

Configuration options for UPDATE operations.

Prop

Type

`returning?`"minimal" | "representation"?

`count?`"exact" | "planned" | "estimated"?

### `DeleteOptions`

Configuration options for DELETE operations.

Prop

Type

`returning?`"minimal" | "representation"?

`count?`"exact" | "planned" | "estimated"?

### `OrderOptions`

Configuration options for ordering operations.

Prop

Type

`ascending?`boolean?

`nullsFirst?`boolean?

`foreignTable?`string?

`referencedTable?`string?

### `LimitOptions`

Configuration options for limit operations.

Prop

Type

`foreignTable?`string?

`referencedTable?`string?

### `RangeOptions`

Configuration options for range operations.

Prop

Type

`foreignTable?`string?

`referencedTable?`string?

### `OrFilterOptions`

Configuration options for OR filter operations.

Prop

Type

`foreignTable?`string?

### `ExplainOptions`

Configuration options for query execution plan explanation.

Prop

Type

`analyze?`boolean?

`verbose?`boolean?

`settings?`boolean?

`buffers?`boolean?

`wal?`boolean?

`format?`"text" | "json" | "yaml"?

### `FunctionsInvokeOptions`

Configuration options for invoking Supabase Edge Functions.

Prop

Type

`headers?`table?

`method?`POST | GET | PUT | DELETE | PATCH

`region?`string?

## Service Client Types

### `RestClient`

Main REST API client interface for Supabase PostgREST operations.

Prop

Type

`from?`function(tableName: string) -> RootQuery

### `SupabaseClient`

The main client type returned by `Supabase.createClient()`. Provides access to all services.

Prop

Type

`rest?`Rest.RestClient

`storage?`Storage.StorageClient

`functions?`EdgeFunctions.FunctionsClient

`_unitTests?`UnitTests.UnitTests

`from?`function(tableName: string) -> RootQuery

`rpc?`function(functionName: string, params: {[string]: any}?) -> RPCFilterStage

## Type Relationships

### Stage-Based Flow Diagram

### Method Availability Matrix

Method Type| Filter Stage| Modifier Stage| Mutation Initial| Mutation Select| Final Stage
---|---|---|---|---|---
**Filter Methods**
(eq, neq, gt, etc.)| ✅ Chainable| ❌ Not available| ❌ Not available| ❌ Not available| ❌ Not available
**Non-Final Modifiers**
(order, limit, range)| ✅ Transitioning| ✅ Chainable| ❌ Not available| ❌ Not available| ❌ Not available
**Finalizers**
(single, maybeSingle)| ✅ Collapses| ✅ Collapses| ❌ Not available*| ✅ Collapses| ❌ Not available
**Finalizers**
(csv, returns)| ✅ Collapses| ✅ Collapses| ✅ Collapses| ✅ Collapses| ❌ Not available
**Finalizers**
(overrideTypes, explain)| ✅ Collapses| ✅ Collapses| ❌ Not available| ❌ Not available| ❌ Not available
**select()**| ✅ (Mutation only)| ❌ Not available| ✅ Transitioning| ❌ Not available| ❌ Not available
**get()**| ✅ (RPC only)| ❌ Not available| ❌ Not available| ❌ Not available| ❌ Not available
**execute()**
**executeTyped()**| ✅ Available| ✅ Available| ✅ Available| ✅ Available| ✅ Only these

_Note:`single()` and `maybeSingle()` are only available in `MutationSelectStage`, not `MutationInitialStage`._

### Query Type to Stage Mapping

Query Type| Initial Stage| After `.select()`| After Modifier| After Finalizer
---|---|---|---|---
`select()`| `SelectFilterStage`| N/A| `SelectModifierStage`| `FinalStage`
`insert()`| `InsertQuery`
(MutationInitialStage)| `MutationSelectStage`| N/A| `FinalStage`
`update()`| `UpdateFilterStage`| `MutationSelectStage`| N/A| `FinalStage`
`delete()`| `DeleteFilterStage`| `MutationSelectStage`| N/A| `FinalStage`
`upsert()`| `UpsertQuery`
(MutationInitialStage)| `MutationSelectStage`| N/A| `FinalStage`
`rpc()`| `RPCFilterStage`| N/A| `RPCModifierStage`| `FinalStage`

[Edge FunctionsInvoke serverless functions on Supabase Edge Runtime.](https://roblox-supabase.vercel.app/docs/EdgeFunctions/edge-functions)


# Installation

Install Roblox Supabase on your Roblox Studio project or using Rojo.

# Prerequisites

To install **Roblox Supabase** on your projects, you need to make sure you have the following:

  * A [Supabase](https://supabase.com/) project set up.
  * Your Supabase project `API_URL` and `API_KEY`.

HTTP Requests disclaimer

Make sure you activated HTTP requests on your Roblox project or any requests to the PostgreSQL endpoint of your Supabase project will fail.

[See more details](https://create.roblox.com/docs/cloud-services/http-service#enable-http-requests).

These are the only steps you must make sure you follow to install **Roblox Supabase** on your project.

# Installation

### Manual Installation

If you prefer installing manually, please follow the steps below:

Download the `.rbxm` file from the [GitHub releases](https://github.com/chteau/Roblox-Supabase/releases/latest) accessible from the Roblox Supabase repository.

Drag and drop the file into your Roblox Studio project.

Place the **"Supabase"** ModuleScript in the `ServerStorage` of your project.

### Using Git Submodules

In order to install **Roblox Supabase** using git submodules, make sure:

  * You are using `Rojo` (or an alternative).
  * That `git` is installed on your computer.

Go to your project's folder and start a new CLI.

In your CLI, enter this command:

    git submodule add https://github.com/chteau/Roblox-Supabase.git

Update your [Rojo configuration file](https://rojo.space/docs/v7/project-format/) to point to the appropriate path and sync **Roblox Supabase** directly in your project.

Update your submodule to get the latest changes.

To keep **Roblox-Supabase** up to date with the latest changes, you can simply enter this command:

    git submodule update --remote

### Using Wally

You can find the package on [this link](https://wally.run/package/chteau/roblox-supabase).

Using wally, make sure that:

  * Aftman is installed.
  * Wally is initialized in your project.

In `wally.toml`, add this line:

    [server-dependencies]
    roblox-supabase = "chteau/roblox-supabase@<THE_LATEST_VERSION>"
    ...

If you use **Rojo**, make sure that in your `default.project.json` file, you reference the `ServerPackage` folder in order for it to be replicated on your studio. (You don't have to put this in `ServerStorage`, but I recommend putting this in.) - See more on [this link](https://alexasterisk.github.io/WallyPackages/import/installation/#using-rojo).

        "name": "project-name"
        "tree": {
            "$className": "DataModel",
            ...
            "ServerStorage": {
                "Packages": {
                    "$path": "ServerPackages"
            },
            ...

In your CLI, enter this command:

    wally install

You can now access **Roblox Supabase** in your server scripts using:

    local SS = game:GetService("ServerStorage")
    local Supabase = require(SS.Packages["roblox-supabase"])

    ...

# Initializing

Once **Roblox Supabase** placed in your Roblox project, you can just set-up a new client in order to use it.

Script.luau

    local Supabase = require("<PATH_TO_SUPABASE>") -- By default, it should be available in "ServerStorage" directly.

    -- Create a single client for interacting with your database
    local client = Supabase.createClient({
        url = "https://xyzcompany.supabase.co",
        apiKey = "publishable-or-anon-key"
    })

### Parameters

Prop

Type

`url`string

`apiKey`string

Options are not supported yet.

Options such as `accessToken`, `auth`, `db`, `global` and `realtime` are yet to be implemented.

If you wish to contribute to the project, see [the contribution docs](https://roblox-supabase.vercel.app/docs/contribute)

There you go! Now you should have a functional Supabase client inside of your Roblox project!

[IntroductionA comprehensive, type-safe Supabase client for Roblox Luau, providing full access to PostgREST API, Storage, and Edge Functions. Built specifically for Roblox's server-side environment with Rojo workflow compatibility.](https://roblox-supabase.vercel.app/docs)[Selecting a tableSelecting a table from your database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/select-table)


# Upsert Data

Insert or update rows in your PostgreSQL database using the Supabase client.

# `upsert(data: {[string]: any} | {[string]: any}[], options: InsertOptions?)`

`upsert()` can be combined with select(), returns(), single(), maybeSingle(), and csv() modifiers.

The `upsert()` method performs an "insert or update" operation on a PostgreSQL table. This is also known as a "merge" operation. It returns an object that allows you to specify what data to return from the upserted rows.

## Method Signature

Prop

Type

`upsert?`function(data: {[string]: any} | {[string]: any}[], options: InsertOptions?)

## Upsert Options

You can configure the upsert operation using [`InsertOptions`](https://roblox-supabase.vercel.app/docs/types#insertoptions) with upsert-specific settings:

## Basic Usage

    -- Upsert a single row by unique constraint
    local result, err = client
        :from("players")
        :upsert(
                username = "Player123",
                level = 5,
                experience = 1000,
                last_login = os.date("!%Y-%m-%d %H:%M:%S")
            },
                onConflict = "username" -- Conflict on username unique constraint
        )
        :execute()

    if err then
        warn("Upsert failed:", err)
    else
        if result[1].created_at == result[1].updated_at then
            print("New player inserted")
        else
            print("Existing player updated")
        end
    end

## Complete Examples

### Example 1: Basic Upsert with Primary Key

    -- Upsert by primary key (id)
    local result, err = client
        :from("players")
        :upsert(
                id = 123,  -- Primary key
                username = "UpdatedPlayer",
                level = 10,
                experience = 5000,
                last_login = os.date("!%Y-%m-%d %H:%M:%S")
            },
                onConflict = "id"
        )
        :select("*")
        :single()
        :execute()

    if err then
        warn("Upsert failed:", err)
    else
        print("Player upserted with ID:", result.id)
    end

### Example 2: Upsert Multiple Rows

    -- Upsert multiple players at once
    local players = {
            username = "Warrior",
            level = 10,
            class = "warrior",
        },
            username = "Mage",
            level = 8,
            class = "mage",
        },
            username = "Rogue",
            level = 12,
            class = "rogue",

    local result, err = client
        :from("players")
        :upsert(players, { onConflict = "username" })
        :select("username, level, class")
        :execute()

    if err then
        warn("Batch upsert failed:", err)
    else
        for _, player in ipairs(result) do
            print(string.format("%s (Level %d %s) upserted",
                player.username, player.level, player.class))
        end
    end

### Example 3: Upsert with Type Validation

    -- Define types for validation
    type PlayerUpsert = {
        id: number?,
        username: string,
        level: number,
        experience: number,
        updated_at: string

    -- Upsert with type validation
    local playerData: PlayerUpsert = {
        username = "TypeSafePlayer",
        level = 15,
        experience = 7500,
        updated_at = os.date("!%Y-%m-%d %H:%M:%S")

    local result, err = client
        :from("players")
        :upsert(playerData, { onConflict = "username" })
        :executeTyped()

    if err then
        warn("Type validation failed:", err)
    end

### Example 4: Upsert with JSON Data

    -- Upsert JSON/JSONB column
    local result, err = client
        :from("player_settings")
        :upsert(
                id = 123,
                settings = {
                    theme = "dark",
                    notifications = {
                        email = true,
                        push = false,
                        in_game = true
                    },
                    preferences = {
                        language = "en",
                        timezone = "UTC"
                },
                last_login = os.date("!%Y-%m-%d %H:%M:%S")
            },
                onConflict = "id"
        )
        :execute()

## Method Chaining Order

### Correct Order

    -- Start with from(), then upsert(), then modifiers
    local result = client
        :from("players")                    -- Step 1: Select table
        :upsert(
            {                               -- Step 2: Upsert data
                username = "TestPlayer",
                level = 1
            },
                onConflict = "username",    -- Step 3: Specify conflict columns (optional but recommended)
        )
        :select("*")                        -- Step 4: Specify return columns (optional)
        :single()                           -- Step 5: Expect single row (optional)
        :execute()                          -- Step 6: Execute

### Important: onConflict option is Recommended

The onConflict option allows you to upsert data using a non-primary unique constraint. You can set these up directly in your database using Supabase's dashboard.

    -- Without onConflict(), upsert uses the primary key by default.
    local result = client
        :from("players")
        :upsert({id = 123, username = "Player"})  -- Assumes conflict on primary key (id)
        :execute()

    -- With onConflict for non-primary unique constraint
    local resultWithConflict = client
        :from("players")
        :upsert(
                username = "Player",
                level = 10
            },
                onConflict = "username"
        )
        :execute()

## Error Handling

### Common Upsert Errors

    -- Missing unique constraint information
    local result, err = client
        :from("players")
        :upsert({
            username = "Player123",
            level = 1
        })
        -- Forgot onConflict - assumes primary key conflict but no primary key in data
        :execute()

    if err then
        warn("Upsert err:", err)  -- May fail if no conflict resolution specified
    end

    -- Multiple unique constraint violations
    local result, err = client
        :from("users")
        :upsert(
                email = "user@example.com",  -- Unique constraint
                username = "user123"         -- Another unique constraint
            },
                onConflict = "email"
        )
        :execute()

    -- Could still fail on username conflict if email is different but username exists

[Update DataUpdate existing rows in your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/update-data)[Delete DataDelete rows from your PostgreSQL database using the Supabase client.](https://roblox-supabase.vercel.app/docs/Database/delete-data)