suparepo

Generate repository, RPC client, and Edge Function client code from Supabase automatically.

Features

  • Repository generation — CRUD operations (getAll, getById, create, update, delete), pagination, count, relation queries
  • RPC client generation — Type-safe Dart methods from Supabase SQL functions (auto-detected via OpenAPI spec, with pg_proc catalog correction for accurate return types)
  • Edge Function client generation — Typed or untyped clients from local supabase/functions/ directory, with automatic TypeScript type inference
  • Type-safe when used with supafreeze models

Installation

dependencies:
  suparepo: ^1.8.0

Quick Start

1. Create Configuration

Create suparepo.yaml in your project root:

url: ${SUPABASE_DATA_API_URL}
secret_key: ${SUPABASE_SECRET_KEY}
output: lib/repositories

# Optional: Link to model classes (generated by supafreeze)
# Use model_import_path for a barrel file import:
model_import_path: package:myapp/models/models.dart
# Or use model_import_prefix for individual file imports (recommended):
# model_import_prefix: package:myapp/

# Optional: Generate barrel file (default: false)
generate_barrel: false

# Optional: Generate Riverpod providers (default: false)
generate_providers: true

# Optional: Filter tables
include:
  - users
  - posts

2. Set Environment Variables

Create .env file:

SUPABASE_DATA_API_URL=https://your-project.supabase.co
SUPABASE_SECRET_KEY=your-service-role-key

3. Run Generator

dart run suparepo

CLI Options

dart run suparepo              # Generate all enabled targets
dart run suparepo --repo       # Generate table repositories only
dart run suparepo --rpc        # Generate RPC client only
dart run suparepo --edge       # Generate Edge Function client only
dart run suparepo --force      # Force regenerate all

RPC Client Generation

Automatically generates type-safe Dart methods for your Supabase SQL functions (RPC).

Configuration

rpc:
  enabled: true
  output: lib/repositories/rpc_client.dart  # optional
  include:
    - get_user_posts
    - search_users
  exclude:
    - internal_cleanup

Generated Code Example

For a SQL function get_user_posts(user_id uuid) returning setof json:

class SupabaseRpcClient {
  final SupabaseClient _client;

  const SupabaseRpcClient(this._client);

  Future<List<Map<String, dynamic>>> getUserPosts({
    required String userId,
  }) async {
    final response = await _client.rpc('get_user_posts', params: {
      'user_id': userId,
    });
    return (response as List).cast<Map<String, dynamic>>();
  }
}

Return Type Correction

PostgREST's OpenAPI spec does not always return accurate type information for RPC functions with scalar return values (boolean, integer, text, etc.) or RETURNS TABLE(...) functions. As a result, methods that should be Future<bool>, Future<String>, or Future<List<Map<String, dynamic>>> may be generated as Future<void>.

suparepo resolves return types in the following priority order:

1. YAML return_types (highest priority, manual)
   ↓ not defined
2. pg_proc auto-correction (when execute_sql exists)
   ↓ execute_sql not available
3. OpenAPI spec (fallback, may produce void)

The simplest approach. Specify return types directly in suparepo.yaml:

rpc:
  enabled: true
  return_types:
    get_my_invite_code: text
    is_active_user: bool
    count_items: int4
    execute_exchange_atomic: void
    get_favorite_products: setof jsonb
  • Values are PostgreSQL type names (text, bool, int4, int8, jsonb, uuid, etc.)
  • Add setof prefix to generate a method returning List<T>
  • These overrides take the highest priority over all other detection methods
  • No need to create an execute_sql function — this is the easiest option

Type mapping reference:

PostgreSQL type Dart type
text, varchar String
bool bool
int4, int8 int
float4, float8 double
jsonb, json Map<String, dynamic>
uuid String
timestamptz DateTime
void void

Option 2: Automatic correction via execute_sql

By creating an execute_sql RPC function in your Supabase project, suparepo can query the PostgreSQL pg_proc catalog directly to auto-detect accurate return types for all RPC functions.

Why is this needed? PostgREST's OpenAPI spec does not always report RPC return types accurately. The execute_sql function allows suparepo to run a SQL query against pg_proc (PostgreSQL's internal function definition table) to retrieve the correct type information.

Run the following in Supabase SQL Editor:

CREATE OR REPLACE FUNCTION execute_sql(query text)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
  result json;
BEGIN
  EXECUTE query INTO result;
  RETURN result;
END;
$$;

Warning: This function runs with SECURITY DEFINER privileges. Ensure it is only callable with the service_role key (which suparepo uses via secret_key). Review your RLS policies and API exposure settings to prevent unauthorized access.

Once created, no additional configuration is needed — suparepo automatically detects execute_sql and applies corrections. Functions using RETURNS TABLE(...) are automatically generated as Future<List<Map<String, dynamic>>>. If execute_sql does not exist, no error is raised and the OpenAPI spec results are used as-is.

Using both return_types and execute_sql: When both are configured, functions listed in return_types use the YAML values, while all other functions are corrected via execute_sql.

Freezed Result Model Generation

For RETURNS TABLE(...) functions, suparepo can automatically generate Freezed result model classes instead of returning List<Map<String, dynamic>>.

Configuration:

rpc:
  enabled: true
  generate_result_models: true
  result_models_output: lib/models/rpc  # optional (default: same directory as rpc client)

Example: For a SQL function:

CREATE FUNCTION get_my_invite_code(user_id uuid)
RETURNS TABLE(code text, max_invites int4, is_active bool)

suparepo generates a Freezed model (get_my_invite_code_result.dart):

@freezed
abstract class GetMyInviteCodeResult with _$GetMyInviteCodeResult {
  const factory GetMyInviteCodeResult({
    required String code,
    required int maxInvites,
    required bool isActive,
  }) = _GetMyInviteCodeResult;

  factory GetMyInviteCodeResult.fromRow(Map<String, dynamic> row) =>
      GetMyInviteCodeResult(
        code: row['code'] as String,
        maxInvites: row['max_invites'] as int,
        isActive: row['is_active'] as bool,
      );
}

And the RPC client method returns the typed model:

Future<List<GetMyInviteCodeResult>> getMyInviteCode({
  required String userId,
}) async {
  final response = await _client.rpc<List<dynamic>>('get_my_invite_code',
      params: {'user_id': userId});
  return response.cast<Map<String, dynamic>>()
      .map(GetMyInviteCodeResult.fromRow).toList();
}

Note: Run build_runner after generation to create the .freezed.dart part files. Requires execute_sql RPC function for TABLE column detection.

Automatic JSON Schema Detection (RETURNS json/jsonb)

For functions that use RETURNS json or RETURNS jsonb, suparepo automatically parses json_build_object() / jsonb_build_object() calls in the function body to detect field names and infer types from variable declarations.

No configuration needed — just enable generate_result_models:

rpc:
  enabled: true
  generate_result_models: true

Example: For a SQL function:

CREATE FUNCTION get_membership_rank_info(p_user_id uuid)
RETURNS json AS $$
DECLARE
  v_rank text;
  v_upload_days int4;
  v_is_active bool;
BEGIN
  -- ... logic ...
  RETURN json_build_object(
    'rank', v_rank,
    'upload_days', v_upload_days,
    'is_active', v_is_active
  );
END;
$$ LANGUAGE plpgsql;

suparepo automatically detects the fields (rank: text, upload_days: int4, is_active: bool) and generates the same Freezed model as RETURNS TABLE functions.

Supported patterns:

  • Variable references with DECLARE types: 'key', v_variable
  • Type cast expressions: 'key', expr::int4
  • json_build_object() and jsonb_build_object()

Note: Requires execute_sql RPC function for pg_proc.prosrc access. If auto-detection fails or the function doesn't use json_build_object, use YAML-defined result_models as a fallback.

Error Code Sealed Class Generation

For RETURNS TABLE(success bool, error text) functions, suparepo automatically detects error code string literals from the PL/pgSQL function body and generates a Freezed sealed class.

Example SQL:

CREATE FUNCTION execute_exchange_atomic(p_mst_id int)
RETURNS TABLE(success bool, error text) AS $$
BEGIN
  IF check_daily_limit() THEN
    return query select false, 'daily_limit_exceeded'::text;
  END IF;
  IF check_balance() THEN
    return query select false, 'insufficient_balance'::text;
  END IF;
  return query select true, ''::text;
END;
$$ LANGUAGE plpgsql;

Generated error class (execute_exchange_atomic_error.dart):

@freezed
sealed class ExecuteExchangeAtomicError
    with _$ExecuteExchangeAtomicError {
  const ExecuteExchangeAtomicError._();

  const factory ExecuteExchangeAtomicError.dailyLimitExceeded()
      = ExecuteExchangeAtomicErrorDailyLimitExceeded;
  const factory ExecuteExchangeAtomicError.insufficientBalance()
      = ExecuteExchangeAtomicErrorInsufficientBalance;
  const factory ExecuteExchangeAtomicError.unknown({
    required String code,
  }) = ExecuteExchangeAtomicErrorUnknown;

  factory ExecuteExchangeAtomicError.fromErrorCode(
      String code) { ... }
}

Generated result model uses the error type:

@freezed
abstract class ExecuteExchangeAtomicResult ... {
  const factory ExecuteExchangeAtomicResult({
    required bool success,
    required ExecuteExchangeAtomicError error,  // typed!
  }) = _ExecuteExchangeAtomicResult;
}

Supported PL/pgSQL patterns:

  • return query select false, 'error_code'::text;
  • error := 'error_code';

Note: Only functions with an error text (or error_code text) column in RETURNS TABLE are analyzed. Requires execute_sql RPC function.

YAML-Defined Result Models (Manual Override)

For functions where auto-detection doesn't work (e.g. no json_build_object, or you want to override the detected schema), you can define the column schema in suparepo.yaml:

rpc:
  enabled: true
  generate_result_models: true
  result_models:
    get_membership_rank_info:
      rank: { type: text }
      upload_days: { type: int4 }
      is_active: { type: bool }
    get_user_profile:
      name: text          # shorthand syntax
      avatar_url: text

Example: For a SQL function:

CREATE FUNCTION get_membership_rank_info(p_user_id uuid)
RETURNS json AS $$
BEGIN
  RETURN json_build_object(
    'rank', v_rank,
    'upload_days', v_upload_days,
    'is_active', v_is_active
  );
END;
$$ LANGUAGE plpgsql;

suparepo generates:

// get_membership_rank_info_result.dart
@freezed
abstract class GetMembershipRankInfoResult
    with _$GetMembershipRankInfoResult {
  const factory GetMembershipRankInfoResult({
    required String rank,
    required int uploadDays,
    required bool isActive,
  }) = _GetMembershipRankInfoResult;

  factory GetMembershipRankInfoResult.fromRow(
    Map<String, dynamic> row,
  ) => GetMembershipRankInfoResult(
    rank: row['rank'] as String,
    uploadDays: row['upload_days'] as int,
    isActive: row['is_active'] as bool,
  );
}

And the RPC client returns a single typed object:

Future<GetMembershipRankInfoResult> getMembershipRankInfo({
  required String pUserId,
}) async {
  final response = await _client.rpc<Map<String, dynamic>>(
    'get_membership_rank_info',
    params: {'p_user_id': pUserId},
  );
  return GetMembershipRankInfoResult.fromRow(response);
}

Note: YAML result_models definitions take precedence over auto-detected RETURNS TABLE columns, allowing you to override the schema if needed.

Edge Function Client Generation

Generates client code for your Supabase Edge Functions by scanning the local supabase/functions/ directory.

Automatic TypeScript Type Inference

suparepo analyzes your Edge Function TypeScript source code and automatically infers request/response types. This eliminates the need to manually define models in YAML.

Supported TypeScript patterns:

// Request: extracted from `body as { ... }` pattern
const { amount, provider } = body as {
  amount?: number;
  provider?: string;
};

// Required detection: inferred from validation if-statements
if (!amount || !provider) {
  return new Response(JSON.stringify({ error: "Missing" }), { status: 400 });
}

// Response: extracted from success response (status 2xx) JSON.stringify()
return new Response(
  JSON.stringify({ exchange_id: data.id, message: "OK" }),
  { status: 200, headers },
);

Type mapping:

TypeScript Dart
string String
number int
boolean bool

Required/optional detection:

  • !field check present → required
  • typeof field !== "type" check present → required
  • ? suffix with no validation → optional

Configuration

Minimal configuration (types are auto-detected from TypeScript):

edge_functions:
  enabled: true
  functions_path: supabase/functions

Full configuration:

edge_functions:
  enabled: true
  functions_path: supabase/functions  # default
  output: lib/repositories/edge_function_client.dart  # optional
  auto_detect_types: true  # Auto-detect types from TypeScript (default: true)
  flatten_request_params: true  # Expand request fields into named params (default: false)
  infer_request_from_usage: true  # Infer request types from handler body usage (default: false)
  include:
    - send-email
  exclude:
    - hello
  # Functions with YAML model definitions take precedence over auto-detection
  models:
    send-email:
      request:
        to: { type: text, required: true }
        subject: { type: text, required: true }
        body_html: { type: text }
      response:
        success: { type: bool, required: true }
        message_id: { type: text }

Note: auto_detect_types is enabled by default. Functions with YAML models definitions take precedence; only undefined functions are inferred from TypeScript. Set auto_detect_types: false to disable.

Generated Code — Without Type Definitions

When auto_detect_types: false and no models defined:

class SupabaseEdgeFunctionClient {
  final SupabaseClient _client;

  const SupabaseEdgeFunctionClient(this._client);

  Future<FunctionResponse> sendEmail({
    Map<String, dynamic>? body,
    Map<String, String>? headers,
  }) async {
    return await _client.functions.invoke(
      'send-email', body: body, headers: headers,
    );
  }
}

Generated Code — With Type Definitions

When types are auto-detected or defined via YAML, typed request/response classes are generated:

class SendEmailRequest {
  final String to;
  final String subject;
  final String? bodyHtml;

  const SendEmailRequest({
    required this.to,
    required this.subject,
    this.bodyHtml,
  });

  Map<String, dynamic> toJson() => {
    'to': to,
    'subject': subject,
    if (bodyHtml != null) 'body_html': bodyHtml,
  };
}

class SendEmailResponse {
  final bool success;
  final String? messageId;

  const SendEmailResponse({required this.success, this.messageId});

  factory SendEmailResponse.fromJson(Map<String, dynamic> json) {
    return SendEmailResponse(
      success: json['success'] as bool,
      messageId: json['message_id'] as String?,
    );
  }
}

Flattened Request Parameters (flatten_request_params)

By default, a typed method takes a single request: wrapper object. Callers construct the request model themselves:

// flatten_request_params: false (default)
await edgeFunctionClient.sendEmail(
  request: SendEmailRequest(to: 'a@example.com', subject: 'Hi'),
);

Set flatten_request_params: true to expand the request fields into named method parameters instead. The JSON body is built inside the generated method, so callers never hardcode JSON string keys:

// flatten_request_params: true
await edgeFunctionClient.sendEmail(
  to: 'a@example.com',
  subject: 'Hi',
);

The generated method builds the body itself:

Future<SendEmailResponse> sendEmail({
  required String to,
  required String subject,
  String? bodyHtml,
  Map<String, String>? headers,
}) async {
  final response = await _client.functions.invoke(
    'send-email',
    body: {
      'to': to,
      'subject': subject,
      if (bodyHtml != null) 'body_html': bodyHtml,
    },
    headers: headers,
  );
  // ...
}

Note: Requires a request model (auto-detected from TypeScript or defined via YAML models). The SendEmailRequest class is still generated for backward compatibility. Required fields are emitted before optional ones to satisfy Dart's parameter ordering.

Usage-based Request Inference (infer_request_from_usage)

By default, request models are only auto-detected from an explicit body as { ... } type annotation. Many Deno Edge Functions instead destructure the body field-by-field with no declared type:

const body = await req.json();
const bondType = body.bond_type;                 // required (validated below)
if (!VALID.includes(bondType)) return badRequest("...");
const nickname = typeof body.nickname === "string" ? body.nickname : null; // optional
const isPinned = body.is_pinned === true;        // boolean, optional

Set infer_request_from_usage: true to recover request models from this style. Inference rules:

  • Fields: every body.<field> access (and req.json() as { ... } casts)
  • Types: typeof body.<field> === "string" | "number" | "boolean" guards, plus body.<field> === true | falsebool
  • Optionality: ternary/nullish defaults, if (body.<field> !== undefined) guards, and boolean-flag comparisons mark a field optional; otherwise required

Note: Inference is heuristic and opt-in. Numeric fields without a typeof === "number" guard cannot be recovered and fall back to text — define an explicit models: entry to override them. YAML models: always take precedence over inference. Combine with flatten_request_params: true to expose the inferred fields as named method parameters.

Error Type Generation (Freezed sealed class)

suparepo automatically detects error responses from your Edge Function TypeScript source and generates Freezed sealed classes for type-safe error handling.

Detected TypeScript pattern:

// Edge Function: create-order
if (!hasStock) {
  return new Response(
    JSON.stringify({ error: "out_of_stock", message: "Item is out of stock" }),
    { status: 400, headers },
  );
}
if (duplicateOrder) {
  return new Response(
    JSON.stringify({ error: "duplicate_order", message: "Already ordered" }),
    { status: 409, headers },
  );
}
  • Scans new Response(...) blocks with status 4xx/5xx
  • Extracts error field string literals in snake_case format
  • Skips generic messages (e.g., "Missing", "Bad request")

Generated code (create_order_error.dart):

@freezed
sealed class CreateOrderError with _$CreateOrderError {
  const CreateOrderError._();

  const factory CreateOrderError.outOfStock({
    required int status,
    required String message,
  }) = CreateOrderErrorOutOfStock;

  const factory CreateOrderError.duplicateOrder({
    required int status,
    required String message,
  }) = CreateOrderErrorDuplicateOrder;

  const factory CreateOrderError.unknown({
    required int status,
    required String errorCode,
    required String message,
  }) = CreateOrderErrorUnknown;

  factory CreateOrderError.fromFunctionException(
    FunctionException e,
  ) {
    final body = jsonDecode(e.details as String)
        as Map<String, dynamic>;
    final code = body['error'] as String? ?? '';
    final msg = body['message'] as String? ?? code;
    return switch (code) {
      'out_of_stock' => CreateOrderError.outOfStock(
          status: e.status, message: msg),
      'duplicate_order' => CreateOrderError.duplicateOrder(
          status: e.status, message: msg),
      _ => CreateOrderError.unknown(
          status: e.status, errorCode: code, message: msg),
    };
  }
}

Usage in your app:

try {
  await edgeFunctionClient.createOrder(request: req);
} on FunctionException catch (e) {
  final error = CreateOrderError.fromFunctionException(e);
  switch (error) {
    case CreateOrderErrorOutOfStock(:final message):
      showSnackBar(message);
    case CreateOrderErrorDuplicateOrder():
      showSnackBar('Already ordered');
    case CreateOrderErrorUnknown(:final errorCode):
      showSnackBar('Error: $errorCode');
  }
}

Error class files are generated alongside the Edge Function client in the same directory. Run build_runner to generate the .freezed.dart part files.

Repository Generation

Generated Code Example

For a users table with columns id, email, name:

class UsersRepository {
  final SupabaseClient _client;

  UsersRepository(this._client);

  Future<List<User>> getAll() async {
    final response = await _client.from('users').select();
    return response.map((e) => User.fromJson(e)).toList();
  }

  Future<User?> getById(String id) async {
    final response = await _client
        .from('users')
        .select()
        .eq('id', id)
        .maybeSingle();
    return response != null ? User.fromJson(response) : null;
  }

  Future<User> create(User data) async { ... }
  Future<User> update(String id, User data) async { ... }
  Future<void> delete(String id) async { ... }
  Future<int> count() async { ... }
  Future<List<User>> paginate({int page = 1, int perPage = 20}) async { ... }
}

Fake Repository Generation

When generate_fakes: true is set, suparepo emits a sibling {table}_repository.fake.dart file alongside each real repository:

output: lib/repositories
generate_fakes: true

The generated Fake{Table}Repository class implements the real repository and backs CRUD with an in-memory Map<dynamic, Model> keyed by primary key:

class FakeUsersRepository implements UsersRepository {
  final Map<dynamic, User> store;

  FakeUsersRepository({Map<dynamic, User>? initial})
      : store = {...?initial};

  void seed(Iterable<User> records) { /* ... */ }

  @override
  Future<List<User>> getAll() async => store.values.toList();

  @override
  Future<User?> getById(String id) async => store[id];

  // ... create / update / delete / count / paginate ...

  // Relation methods fall back to getAll() (relations are not embedded).
  @override
  Future<List<User>> getAllWithCompany() => getAll();

  // Custom methods (from .custom.dart) are stubbed — override in tests.
  @override
  Future<User?> getActive()
      => throw UnimplementedError(
        'FakeUsersRepository.getActive is not faked. '
        'Override in a subclass to provide test behavior.',
      );
}

Use it in tests by injecting the fake via Riverpod overrides:

final fake = FakeUsersRepository()..seed([alice, bob]);
final container = ProviderContainer(overrides: [
  usersRepositoryProvider.overrideWith((ref) => fake),
]);

Custom methods and relation methods are intentionally stubbed because their semantics (.custom.dart SQL, Supabase relation embeds) can't be replicated in memory — subclass the fake in your test file to provide specific behavior when needed.

Riverpod Provider Generation

When generate_providers: true is set, each repository and RPC client gets a @Riverpod(keepAlive: true) provider:

@Riverpod(keepAlive: true)
UsersRepository usersRepository(Ref ref) {
  final client = ref.watch(supabaseClientProvider);
  return UsersRepository(client);
}

A supabase_client_provider.dart is also generated, which must be overridden in your ProviderScope:

@Riverpod(keepAlive: true)
SupabaseClient supabaseClient(Ref ref) {
  throw UnimplementedError(
    'supabaseClientProvider must be overridden in ProviderScope.',
  );
}

Custom Provider Output Path

By default, supabase_client_provider.dart is generated in the same directory as repositories. To output it to a different location (e.g., a Gateway package), use client_provider_output and client_provider_import:

generate_providers: true
client_provider_output: ../gateway/lib/supabase/supabase_client_provider.dart
client_provider_import: package:gateway/supabase/supabase_client_provider.dart
  • client_provider_output — File path where supabase_client_provider.dart is written
  • client_provider_import — Import path used in generated repository/RPC code to reference the provider

Model Import Options

There are two ways to link generated repositories to supafreeze models:

Barrel file import (model_import_path):

model_import_path: package:myapp/models/models.dart

All repositories import from a single barrel file.

Individual file import (model_import_prefix, recommended):

model_import_prefix: package:myapp/

Each repository imports its own model file (e.g., package:myapp/users.supafreeze.dart). Takes precedence over model_import_path.

Full Configuration Reference

url: ${SUPABASE_DATA_API_URL}
secret_key: ${SUPABASE_SECRET_KEY}
output: lib/repositories
schema: public
fetch: always                # always | if_no_cache | never
generate_barrel: false       # Generate barrel file for repositories (default: false)
generate_fakes: false        # Generate in-memory *.fake.dart files for tests (default: false)
generate_providers: true     # Generate Riverpod providers (default: false)
client_provider_output: ../gateway/lib/supabase/supabase_client_provider.dart  # Custom output path
client_provider_import: package:gateway/supabase/supabase_client_provider.dart # Custom import path
model_import_path: package:myapp/models/models.dart       # Barrel file import
model_import_prefix: package:myapp/                       # Individual file import (takes precedence)
supabase_import: package:supabase_flutter/supabase_flutter.dart  # or package:supabase/supabase.dart for pure Dart

# Table filter (for repository generation)
include: [users, posts]
# exclude: [_migrations]

# RPC function client
rpc:
  enabled: true
  output: lib/repositories/rpc_client.dart
  include: [get_user_posts]
  exclude: [internal_cleanup]
  return_types:                 # Manual return type overrides (see "Return Type Correction")
    get_invite_code: text
    is_active: bool
    get_items: setof jsonb
  generate_result_models: true  # Generate Freezed result models for RETURNS TABLE functions (default: false)
  result_models_output: lib/models/rpc  # Custom output directory for result models (optional)
  result_models:                # YAML-defined schemas for RETURNS json/jsonb functions
    get_membership_rank_info:
      rank: { type: text }
      upload_days: { type: int4 }
      is_active: { type: bool }

# Edge Function client
edge_functions:
  enabled: true
  output: lib/repositories/edge_function_client.dart
  functions_path: supabase/functions
  auto_detect_types: true    # Auto-detect types from TypeScript (default: true)
  include: [send-email]
  # exclude: [hello]
  # Functions with YAML models take precedence; others are auto-detected from TS
  models:
    send-email:
      request:
        to: { type: text, required: true }
        subject: { type: text, required: true }
      response:
        success: { type: bool, required: true }

Using with supafreeze

For the best experience, use suparepo together with supafreeze:

  1. Generate models with supafreeze
  2. Set model_import_prefix (or model_import_path) in suparepo.yaml to point to your models
  3. Optionally enable generate_providers: true for Riverpod DI
  4. Generate repositories with suparepo

This gives you fully type-safe repositories with Freezed models and optional Riverpod providers.

License

MIT

Libraries

suparepo
Generate repository/data access layer code from Supabase database schema.