npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ngx-saqly-supabase

v0.0.18

Published

Angular library for building full-stack applications with [Supabase](https://supabase.com) — **no backend required**.

Downloads

1,820

Readme

ngx-supabase

Angular library for building full-stack applications with Supabaseno backend required.

Define your database schema in TypeScript, run migrations, and perform full CRUD operations directly from Angular using a simple, type-safe API.


Table of Contents

  1. Installation
  2. Quick Setup
  3. Step 1 — Define Your Schema
  4. Step 2 — Run Migrations
  5. Step 3 — CRUD Operations
  6. Step 4 — Querying Across Tables (Joins)
  7. Relations
  8. Row-Level Security & Policies
  9. Authentication
  10. Complete Example — Products
  11. Real Example — Users, Products & Categories
  12. API Reference

Installation

npm install ngx-saqly-supabase @supabase/supabase-js

Quick Setup

Add the provider once in app.config.ts:

// app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideSaqlySupabase } from 'ngx-saqly-supabase';
import { environment } from '../environments/environment';
import { productsMigration } from './schema/product';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),
    provideSaqlySupabase({
      url: environment.supabaseUrl,
      key: environment.supabaseAnonKey,
      migrations: [productsMigration],
    }),
  ],
};

Step 1 — Define Your Schema

defineSchema(tableName, fields)

Defines a database table and its columns. The TypeScript type of each row is inferred automatically — no interface needed.

// schema/product.ts
import { defineSchema, defineModel, field, InferModel, MigrationDefinition } from 'ngx-saqly-supabase';

export const ProductsTable = defineSchema('products', {
  id:          field.bigint().primaryGenerated(),   // auto-generated PK
  name:        field.string().required(),           // NOT NULL
  price:       field.integer().required(),          // NOT NULL
  stock:       field.integer().default('0'),        // always present (has default)
  description: field.string(),                      // nullable — optional
  created_at:  field.timestamp().defaultNow(),
});

export type Product = InferModel<typeof ProductsTable>;
// → { id: number; name: string; price: number; stock: number; description?: string; created_at: string }

export const ProductModel = defineModel(ProductsTable);

Field Types

| Builder | PostgreSQL | TypeScript | |---|---|---| | field.string() | text | string | | field.text() | text | string | | field.uuid() | uuid | string | | field.integer() | integer | number | | field.bigint() | bigint | number | | field.boolean() | boolean | boolean | | field.timestamp() | timestamp with time zone | string | | field.jsonb() | jsonb | unknown |

Field Modifiers

| Modifier | SQL Effect | TypeScript Effect | |---|---|---| | .required() | NOT NULL | required property | | .nullable() | allow NULL | optional property (\| undefined) | | .primary() | PRIMARY KEY | required property | | .primaryGenerated() | GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY | required property | | .unique() | UNIQUE | no change | | .default('value') | DEFAULT value | required property (always present) | | .defaultNow() | DEFAULT now() | required property | | .references(table, column, options?) | inline REFERENCES | no change |

Type Inference Rules

// REQUIRED in TypeScript (always present when reading)
id:         field.bigint().primaryGenerated()  // auto-generated
name:       field.string().required()          // NOT NULL
status:     field.string().default(`'open'`)   // has DB default
created_at: field.timestamp().defaultNow()     // has DB default

// OPTIONAL in TypeScript (can be NULL)
notes:      field.string()   // → string | undefined

Step 2 — Run Migrations

Migrations convert your TypeScript schema to SQL and execute it against Supabase. All statements are idempotent — re-running the same migration is always safe:

  • CREATE TABLE IF NOT EXISTS
  • ADD COLUMN IF NOT EXISTS
  • DROP POLICY IF EXISTS before every CREATE POLICY

What the generated SQL looks like

When you call preview() or download(), the output is ready to paste into Supabase SQL Editor:

-- Setup
create or replace function exec_sql(sql_query text)
returns json language plpgsql security definer as $$
begin
  execute sql_query;
  return '{"success": true}'::json;
exception when others then
  return json_build_object('error', sqlerrm);
end;
$$;

grant execute on function exec_sql(text) to anon, authenticated;

-- Migration: create-products-table

-- Tables
create table if not exists public.products (
  id bigint generated by default as identity primary key,
  name text not null,
  price integer not null,
  stock integer default 0,
  description text,
  created_at timestamp with time zone default now()
);

-- Row Level Security
alter table public.products enable row level security;

-- Policies
drop policy if exists "Anyone can read products" on public.products;
create policy "Anyone can read products"
on public.products
for SELECT
to anon, authenticated
using (true);

drop policy if exists "Anyone can insert products" on public.products;
create policy "Anyone can insert products"
on public.products
for INSERT
to anon, authenticated
with check (true);

-- Grants
grant usage on schema public to anon, authenticated;
grant SELECT, INSERT on table public.products to anon, authenticated;
grant usage, select on sequence public.products_id_seq to anon, authenticated;

Copy the whole output → paste into Supabase → SQL Editor → click Run. Done.

Every policy is preceded by DROP POLICY IF EXISTS, so running this SQL again (or after running it manually) never fails with "policy already exists".


Step A — Preview and copy SQL (no keys needed)

Create an AdminMigrationComponent — it checks migration status on load and shows pending SQL automatically, no button click needed.

// admin-migration.component.ts
import { Component, OnInit, signal } from '@angular/core';
import { injectMigration, MigrationRunner, MigrationRunResult } from 'ngx-saqly-supabase';
import { productsMigration } from '../schema/product';

// Add every migration here — already-applied ones are skipped automatically
const ALL_MIGRATIONS = [productsMigration];

@Component({
  selector: 'app-admin-migration',
  standalone: true,
  template: `
    <section style="padding: 24px; max-width: 860px; margin: auto; font-family: sans-serif;">

      <!-- one-time Supabase setup banner -->
      @if (setupReady() === false) {
        <div style="border: 1px solid #f90; border-radius: 8px; padding: 16px; margin-bottom: 24px; background: #fffbe6;">
          <strong>⚠ One-time setup required</strong>
          <p style="margin: 8px 0 12px;">
            Run this SQL once in your
            <a href="https://supabase.com/dashboard" target="_blank">Supabase SQL Editor</a>
            to enable automatic migrations without a <code>managementKey</code>.
          </p>
          <pre style="background: #111; color: #0f0; padding: 16px; border-radius: 6px; overflow: auto; font-size: 13px;">{{ setupSql }}</pre>
          <button (click)="copySetupSql()">{{ copied() ? '✓ Copied!' : 'Copy SQL' }}</button>
          <button (click)="recheckSetup()" style="margin-left: 8px;">I've run it — re-check</button>
        </div>
      }

      @if (setupReady() === true) {
        <p style="color: green; margin-bottom: 16px;">✓ exec_sql is ready</p>
      }

      <!-- migration status — loads automatically on init -->
      @if (setupReady() !== null) {
        <div style="border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 16px;">
          <span style="font-size: 12px; background: #eee; border-radius: 4px; padding: 2px 8px;">Step 1</span>
          <h2 style="margin: 8px 0 4px;">Migrations</h2>

          @if (loading()) {
            <p style="color: #888; margin-top: 12px;">Checking migrations…</p>
          }

          @if (!loading() && pendingCount() === 0) {
            <p style="color: green; margin-top: 12px;">✓ All migrations are up to date.</p>
          }

          @if (!loading() && pendingCount() > 0) {
            <p style="margin: 12px 0 8px; color: #555;">
              {{ pendingCount() }} pending migration(s) — copy the SQL below and run it in Supabase SQL Editor.
            </p>
            <pre style="background: #111; color: #0f0; padding: 16px; border-radius: 6px; overflow: auto; font-size: 13px;">{{ sql() }}</pre>
          }
        </div>
      }

      <!-- run results (shown after runMany) -->
      @if (runResults().length) {
        <div style="border: 1px solid #ddd; border-radius: 8px; padding: 16px;">
          <h3 style="margin: 0 0 12px;">Run Results</h3>
          @for (r of runResults(); track r.name) {
            <div style="margin-bottom: 8px;">
              @if (r.result.success) {
                <span style="color: green;">✓ {{ r.name }}</span>
              } @else {
                <span style="color: red;">✗ {{ r.name }} — {{ r.result.error }}</span>
              }
            </div>
          }
        </div>
      }

    </section>
  `,
})
export class AdminMigrationComponent implements OnInit {
  private readonly migration = injectMigration();

  readonly setupSql = MigrationRunner.setupSql();
  setupReady   = signal<boolean | null>(null);
  copied       = signal(false);
  loading      = signal(true);
  sql          = signal('');
  pendingCount = signal(0);
  runResults   = signal<MigrationRunResult[]>([]);

  async ngOnInit(): Promise<void> {
    this.setupReady.set(await this.migration.checkSetup());
    await this.loadPending();
  }

  async recheckSetup(): Promise<void> {
    this.setupReady.set(null);
    this.setupReady.set(await this.migration.checkSetup());
    await this.loadPending();
  }

  copySetupSql(): void {
    navigator.clipboard.writeText(this.setupSql).then(() => {
      this.copied.set(true);
      setTimeout(() => this.copied.set(false), 2000);
    });
  }

  private async loadPending(): Promise<void> {
    this.loading.set(true);
    const pending = await this.migration.getPending(ALL_MIGRATIONS);
    this.pendingCount.set(pending.length);
    this.sql.set(await this.migration.previewMany(ALL_MIGRATIONS));
    this.loading.set(false);
  }
}

To add more migrations later, just append them to ALL_MIGRATIONS:

const ALL_MIGRATIONS = [
  productsMigration,            // ← already applied, skipped
  addStockToProductsMigration,  // ← new, shown in pending SQL
];

Step B — Run automatically on app startup

Pass your migrations to provideSaqlySupabase — they run once on startup and are tracked automatically. Only new (not-yet-applied) migrations run; already-applied ones are silently skipped.

// app.config.ts
provideSaqlySupabase({
  url: environment.supabaseUrl,
  key: environment.supabaseAnonKey,
  migrations: [
    productsMigration,            // ← skipped if already applied
    addStockToProductsMigration,  // ← runs only once, then tracked
  ],
})

Console output on startup:

[ngx-supabase] ✓ Migration "create-products-table" applied
[ngx-supabase] ✓ Migration "add-stock-to-products" applied

Use Step A or Step B — or both together:

| Scenario | Recommendation | |---|---| | Dev / staging — you want to see the SQL before it runs | Step A | | Production — run migrations silently on deploy | Step B | | Both — see status in UI + auto-run on startup | Step A + Step B together |

Execution modes — pick one:

| What you provide | How migrations run | |---|---| | managementKey | Supabase Management API — no setup required | | serviceRoleKey only | exec_sql RPC with service-role client | | Neither | exec_sql RPC with anon client — run MigrationRunner.setupSql() once first |


Grants — automatically included

The generated SQL automatically includes GRANT statements derived from your policies. No manual SQL needed after enabling RLS.

| Policy operation | SQL privileges granted | |---|---| | select | SELECT | | insert | INSERT, SELECT | | update | UPDATE, SELECT | | delete | DELETE, SELECT | | all | SELECT, INSERT, UPDATE, DELETE |


MigrationDefinition

import { MigrationDefinition } from 'ngx-saqly-supabase';

export const productsMigration: MigrationDefinition = {
  name: 'create-products-table',  // unique name — used for tracking
  tables: [ProductsTable],        // CREATE TABLE IF NOT EXISTS
  alterations: [],                // ALTER TABLE statements (see below)
  relations: [],                  // foreign key constraints
  rls: [ProductsTable],           // enable RLS
  policies: [ReadPolicy, InsertPolicy, UpdatePolicy, DeletePolicy],
};

Schema Alterations — adding or removing columns

When a table already exists and you need to change its structure, create a new MigrationDefinition using alter(). No SQL knowledge required — the library generates ALTER TABLE statements automatically.

import { alter, field, MigrationDefinition } from 'ngx-saqly-supabase';
import { ProductsTable } from './product';

// Each schema change = one new migration with a unique name
export const addStockToProductsMigration: MigrationDefinition = {
  name: 'add-stock-to-products',
  alterations: [
    alter(ProductsTable).addColumn('stock', field.integer().default('0')),
  ],
};

export const addDescriptionMigration: MigrationDefinition = {
  name: 'add-description-to-products',
  alterations: [
    alter(ProductsTable).addColumn('description', field.string()),
    alter(ProductsTable).dropColumn('legacy_notes'),
    alter(ProductsTable).renameColumn('qty', 'stock'),
  ],
};

Generated SQL (produced automatically, no manual writing):

alter table public.products add column if not exists stock integer default 0;
alter table public.products drop column if exists legacy_notes;
alter table public.products rename column qty to stock;

alter(table) operations:

| Method | SQL | |---|---| | .addColumn(name, field) | ADD COLUMN IF NOT EXISTS … | | .dropColumn(name) | DROP COLUMN IF EXISTS … | | .renameColumn(old, new) | RENAME COLUMN … TO … |

Add all your migrations (old and new) to the same array — already-applied ones are skipped automatically:

const ALL_MIGRATIONS = [
  productsMigration,           // ← already applied, skipped
  addStockToProductsMigration, // ← new, will run
];

Step 3 — CRUD Operations

Inject the repository

import { injectModel } from 'ngx-saqly-supabase';
import { ProductModel } from './schema/product';

const repo = injectModel(ProductModel);

Read

// All rows
const all = await repo.findAll();

// With filters, ordering, pagination
const latest = await repo.findMany({
  where: { stock: 0 },
  orderBy: 'created_at',
  ascending: false,
  limit: 20,
  offset: 0,
});

// Select specific columns
const names = await repo.findMany({ select: 'id, name, price' });

// First match
const one = await repo.findFirst({ where: { name: 'Widget' } });

// By primary key
const byId = await repo.findById(42);

Write

All write methods accept WritePayload<T> — a flat optional version of your model type. Every field is optional and accepts null, so you only pass what you need.

// Insert — returns the created record
const created = await repo.insert({ name: 'Widget', price: 999, stock: 50 });

// Insert many
const many = await repo.insertMany([
  { name: 'A', price: 10 },
  { name: 'B', price: 20 },
]);

// Upsert (insert or update by PK)
const upserted = await repo.upsert({ id: 1, name: 'Widget', price: 899 });

// Update by PK — pass only the fields you want to change
const updated = await repo.updateById(1, { price: 799 });

// Update many rows matching a filter
await repo.updateMany({ stock: 0 }, { stock: 100 });

You can import WritePayload<T> if you want to type a helper variable explicitly:

import { WritePayload } from 'ngx-saqly-supabase';
import { Product } from './schema/product';

const payload: WritePayload<Product> = {
  name: 'Widget',
  price: 999,
};

await repo.insert(payload);

Delete

await repo.deleteById(1);
await repo.deleteMany({ stock: 0 });

Aggregate

const total = await repo.count();
const lowStock = await repo.count({ where: { stock: 0 } });
const exists = await repo.exists({ where: { name: 'Widget' } });

Step 4 — Querying Across Tables (Joins)

The select option passes directly to Supabase's PostgREST selector, which supports nested joins in a single request.

Syntax

column, foreignTable(col1, col2)          -- left join (null if no match)
column, foreignTable!inner(col1, col2)    -- inner join (excludes non-matches)
alias:foreignTable(col1, col2)            -- with alias

Product with category

const products = await repo.findMany({
  select: 'id, name, price, category:categories(id, name)',
});
// [{ id: 1, name: 'Widget', category: { id: 3, name: 'Electronics' } }]

Product with category and owner

const products = await repo.findMany({
  select: 'id, name, price, category:categories(id, name), owner:users(id, full_name)',
});

Nested (order → items → product)

const orders = await orderRepo.findMany({
  select: 'id, status, items:order_items(quantity, product:products(id, name, price))',
});

Relations

Relations add foreign key constraints to the database via migrations.

One-to-One

import { relation } from 'ngx-saqly-supabase';

const UserProfileRelation = relation.oneToOne(ProfilesTable, UsersTable, {
  localKey: 'user_id',
  foreignKey: 'id',
  onDelete: 'cascade',
});

One-to-Many

const CategoryProductsRelation = relation.oneToMany(CategoriesTable, ProductsTable, {
  localKey: 'id',
  foreignKey: 'category_id',
});

Many-to-One

const ProductCategoryRelation = relation.manyToOne(ProductsTable, CategoriesTable, {
  localKey: 'category_id',
  foreignKey: 'id',
  onDelete: 'set null',
});

Many-to-Many (via junction table)

export const ProductTagsTable = defineSchema('product_tags', {
  product_id: field.bigint().required(),
  tag_id:     field.bigint().required(),
});

const ToProduct = relation.manyToOne(ProductTagsTable, ProductsTable, {
  localKey: 'product_id', foreignKey: 'id', onDelete: 'cascade',
});

const ToTag = relation.manyToOne(ProductTagsTable, TagsTable, {
  localKey: 'tag_id', foreignKey: 'id', onDelete: 'cascade',
});

Query:

const products = await repo.findMany({
  select: 'id, name, tags:product_tags(tag:tags(id, name))',
});

onDelete / onUpdate

| Value | Effect | |---|---| | 'cascade' | Delete/update child rows automatically | | 'restrict' | Prevent delete/update if children exist | | 'set null' | Set FK to NULL on parent delete | | 'no action' | Default — error if constraint violated |


Row-Level Security & Policies

import { policy } from 'ngx-saqly-supabase';

const ReadOwnPolicy = policy
  .select('Users read own tasks')
  .on(TasksTable)
  .to('authenticated')
  .using('auth.uid() = user_id')
  .build();

const InsertOwnPolicy = policy
  .insert('Users insert own tasks')
  .on(TasksTable)
  .to('authenticated')
  .withCheck('auth.uid() = user_id')
  .build();

Operations: select · insert · update · delete · all


Authentication

import { injectAuth } from 'ngx-saqly-supabase';

export class AppComponent {
  readonly auth = injectAuth();
  // auth.user()    → Signal<User | null>
  // auth.loading() → Signal<boolean>
}
const result = await this.auth.signUp('[email protected]', 'password');
if (result.confirmationRequired) { /* email sent */ }

const result = await this.auth.signIn('[email protected]', 'password');
if (result.error) console.error(result.error);

await this.auth.signOut();
@if (auth.loading()) { <p>Loading...</p> }

@if (auth.user()) {
  <p>Welcome, {{ auth.user()?.email }}</p>
  <button (click)="auth.signOut()">Sign out</button>
}

@if (!auth.loading() && !auth.user()) {
  <!-- show login form -->
}

Complete Example — Products

A full working app from schema to component — a single products table, step by step.

File structure

src/
├── environments/
│   └── environment.ts
├── app.config.ts
├── app.component.ts
├── schema/
│   └── product.ts
└── products/
    ├── products.component.ts
    └── admin-migration.component.ts

1. schema/product.ts

Define the table, the model, the policies, and the migration — all in one file.

import {
  defineSchema,
  defineModel,
  field,
  policy,
  InferModel,
  MigrationDefinition,
} from 'ngx-saqly-supabase';

// ── Table ──────────────────────────────────────────────────────────────────
export const ProductsTable = defineSchema('products', {
  id:          field.bigint().primaryGenerated(),
  name:        field.string().required(),
  price:       field.integer().required(),
  stock:       field.integer().default('0'),
  description: field.string(),
  created_at:  field.timestamp().defaultNow(),
});

export type Product = InferModel<typeof ProductsTable>;
// → { id: number; name: string; price: number; stock: number; description?: string; created_at: string }

export const ProductModel = defineModel(ProductsTable);

// ── Policies ───────────────────────────────────────────────────────────────
const PublicReadPolicy = policy
  .select('Anyone can read products')
  .on(ProductsTable)
  .to('anon', 'authenticated')
  .using('true')
  .build();

const PublicInsertPolicy = policy
  .insert('Anyone can insert products')
  .on(ProductsTable)
  .to('anon', 'authenticated')
  .withCheck('true')
  .build();

const PublicUpdatePolicy = policy
  .update('Anyone can update products')
  .on(ProductsTable)
  .to('anon', 'authenticated')
  .using('true')
  .withCheck('true')
  .build();

const PublicDeletePolicy = policy
  .delete('Anyone can delete products')
  .on(ProductsTable)
  .to('anon', 'authenticated')
  .using('true')
  .build();

// ── Migration ──────────────────────────────────────────────────────────────
export const productsMigration: MigrationDefinition = {
  name: 'create-products-table',
  tables:   [ProductsTable],
  rls:      [ProductsTable],
  policies: [PublicReadPolicy, PublicInsertPolicy, PublicUpdatePolicy, PublicDeletePolicy],
};

2. app.config.ts

Register Supabase and pass the migration. It runs once automatically — already-applied migrations are skipped on every subsequent start.

import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideSaqlySupabase } from 'ngx-saqly-supabase';
import { environment } from '../environments/environment';
import { productsMigration } from './schema/product';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),
    provideSaqlySupabase({
      url: environment.supabaseUrl,
      key: environment.supabaseAnonKey,
      migrations: [productsMigration],
    }),
  ],
};

3. products/admin-migration.component.ts

Shows migration status on load. If exec_sql is not set up yet, it shows the one-time SQL to copy and run in Supabase SQL Editor.

import { Component, OnInit, signal } from '@angular/core';
import { injectMigration, MigrationRunner, MigrationRunResult } from 'ngx-saqly-supabase';
import { productsMigration } from '../schema/product';

const ALL_MIGRATIONS = [productsMigration];

@Component({
  selector: 'app-admin-migration',
  standalone: true,
  template: `
    <section style="padding: 24px; max-width: 860px; margin: auto; font-family: sans-serif;">

      @if (setupReady() === false) {
        <div style="border: 1px solid #f90; border-radius: 8px; padding: 16px; margin-bottom: 24px; background: #fffbe6;">
          <strong>⚠ One-time setup required</strong>
          <p style="margin: 8px 0 12px;">
            Run this SQL once in your
            <a href="https://supabase.com/dashboard" target="_blank">Supabase SQL Editor</a>.
          </p>
          <pre style="background: #111; color: #0f0; padding: 16px; border-radius: 6px; overflow: auto; font-size: 13px;">{{ setupSql }}</pre>
          <button (click)="copySetupSql()">{{ copied() ? '✓ Copied!' : 'Copy SQL' }}</button>
          <button (click)="recheckSetup()" style="margin-left: 8px;">I've run it — re-check</button>
        </div>
      }

      @if (setupReady() === true) {
        <p style="color: green; margin-bottom: 16px;">✓ exec_sql is ready</p>
      }

      @if (setupReady() !== null) {
        <div style="border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 16px;">
          <span style="font-size: 12px; background: #eee; border-radius: 4px; padding: 2px 8px;">Migrations</span>
          <h2 style="margin: 8px 0 4px;">Status</h2>

          @if (loading()) {
            <p style="color: #888; margin-top: 12px;">Checking migrations…</p>
          }

          @if (!loading() && pendingCount() === 0) {
            <p style="color: green; margin-top: 12px;">✓ All migrations are up to date.</p>
          }

          @if (!loading() && pendingCount() > 0) {
            <p style="margin: 12px 0 8px; color: #555;">
              {{ pendingCount() }} pending migration(s) — copy the SQL below and run it in Supabase SQL Editor.
            </p>
            <pre style="background: #111; color: #0f0; padding: 16px; border-radius: 6px; overflow: auto; font-size: 13px;">{{ sql() }}</pre>
          }
        </div>
      }

      @if (runResults().length) {
        <div style="border: 1px solid #ddd; border-radius: 8px; padding: 16px;">
          <h3 style="margin: 0 0 12px;">Run Results</h3>
          @for (r of runResults(); track r.name) {
            <div style="margin-bottom: 8px;">
              @if (r.result.success) {
                <span style="color: green;">✓ {{ r.name }}</span>
              } @else {
                <span style="color: red;">✗ {{ r.name }} — {{ r.result.error }}</span>
              }
            </div>
          }
        </div>
      }

    </section>
  `,
})
export class AdminMigrationComponent implements OnInit {
  private readonly migration = injectMigration();

  readonly setupSql = MigrationRunner.setupSql();
  setupReady   = signal<boolean | null>(null);
  copied       = signal(false);
  loading      = signal(true);
  sql          = signal('');
  pendingCount = signal(0);
  runResults   = signal<MigrationRunResult[]>([]);

  async ngOnInit(): Promise<void> {
    this.setupReady.set(await this.migration.checkSetup());
    await this.loadPending();
  }

  async recheckSetup(): Promise<void> {
    this.setupReady.set(null);
    this.setupReady.set(await this.migration.checkSetup());
    await this.loadPending();
  }

  copySetupSql(): void {
    navigator.clipboard.writeText(this.setupSql).then(() => {
      this.copied.set(true);
      setTimeout(() => this.copied.set(false), 2000);
    });
  }

  private async loadPending(): Promise<void> {
    this.loading.set(true);
    const pending = await this.migration.getPending(ALL_MIGRATIONS);
    this.pendingCount.set(pending.length);
    this.sql.set(await this.migration.previewMany(ALL_MIGRATIONS));
    this.loading.set(false);
  }
}

4. products/products.component.ts

Full CRUD component — reads, inserts, updates, and deletes products.

import { Component, OnInit, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { injectModel } from 'ngx-saqly-supabase';
import { Product, ProductModel } from '../schema/product';

@Component({
  selector: 'app-products',
  standalone: true,
  imports: [FormsModule],
  template: `
    <section style="padding: 24px; max-width: 800px; margin: auto;">
      <h1>Products</h1>

      @if (message()) {
        <p>{{ message() }}</p>
      }

      <div style="display: grid; gap: 12px; margin-bottom: 24px;">
        <input type="text"   placeholder="Product name"  [(ngModel)]="name" />
        <input type="number" placeholder="Price"         [(ngModel)]="price" />
        <input type="number" placeholder="Stock"         [(ngModel)]="stock" />
        <textarea            placeholder="Description"   [(ngModel)]="description"></textarea>

        <button (click)="addProduct()">Add Product</button>
        <button (click)="loadProducts()">Reload</button>
        <button (click)="countProducts()">Count</button>
      </div>

      <h3>Total Products: {{ total() }}</h3>

      @for (product of products(); track product.id) {
        <div style="border: 1px solid #ddd; padding: 16px; margin-bottom: 12px; border-radius: 8px;">
          <h3>{{ product.name }}</h3>
          <p>Price: {{ product.price }}</p>
          <p>Stock: {{ product.stock }}</p>
          <p>Description: {{ product.description || 'No description' }}</p>
          <button (click)="updateProduct(product)">+10 Price</button>
          <button (click)="deleteProduct(product.id)">Delete</button>
        </div>
      }

      @if (products().length === 0) {
        <p>No products found.</p>
      }
    </section>
  `,
})
export class ProductsComponent implements OnInit {
  private readonly repo = injectModel(ProductModel);

  products = signal<Product[]>([]);
  total    = signal(0);
  message  = signal('');

  name        = '';
  price       = 0;
  stock       = 0;
  description = '';

  async ngOnInit(): Promise<void> {
    await this.loadProducts();
    await this.countProducts();
  }

  async loadProducts(): Promise<void> {
    try {
      this.products.set(await this.repo.findMany({
        orderBy: 'created_at',
        ascending: false,
      }));
      this.message.set('Products loaded');
    } catch {
      this.message.set('Error loading products');
    }
  }

  async addProduct(): Promise<void> {
    if (!this.name.trim()) { this.message.set('Name is required'); return; }
    if (Number(this.price) <= 0) { this.message.set('Price must be > 0'); return; }
    try {
      await this.repo.insert({
        name:        this.name.trim(),
        price:       Number(this.price),
        stock:       Number(this.stock),
        description: this.description.trim() || undefined,
      });
      this.name = ''; this.price = 0; this.stock = 0; this.description = '';
      await this.loadProducts();
      await this.countProducts();
      this.message.set('Product added');
    } catch {
      this.message.set('Error adding product');
    }
  }

  async updateProduct(product: Product): Promise<void> {
    try {
      await this.repo.updateById(product.id, { price: product.price + 10 });
      await this.loadProducts();
      this.message.set('Product updated');
    } catch {
      this.message.set('Error updating product');
    }
  }

  async deleteProduct(id: number): Promise<void> {
    try {
      await this.repo.deleteById(id);
      await this.loadProducts();
      await this.countProducts();
      this.message.set('Product deleted');
    } catch {
      this.message.set('Error deleting product');
    }
  }

  async countProducts(): Promise<void> {
    try {
      this.total.set(await this.repo.count());
    } catch {
      this.message.set('Error counting products');
    }
  }
}

5. app.component.ts

Wire the two components into the root component.

import { Component } from '@angular/core';
import { ProductsComponent } from './products/products.component';
import { AdminMigrationComponent } from './products/admin-migration.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ProductsComponent, AdminMigrationComponent],
  template: `
    <app-admin-migration />
    <app-products />
  `,
})
export class App {}

Real Example — Users, Products & Categories

A full multi-table schema split across separate files — the recommended structure for real projects.

File structure

src/
├── environments/
│   └── environment.ts
├── app.config.ts
├── app.component.ts
├── schema/
│   ├── user.ts
│   ├── category.ts
│   └── product.ts       ← imports from user.ts and category.ts
└── products/
    ├── products.component.ts
    └── admin-migration.component.ts

1. schema/user.ts

import { defineSchema, defineModel, field, policy, InferModel } from 'ngx-saqly-supabase';

// ── Table ──────────────────────────────────────────────────────────────────
export const UsersTable = defineSchema('users', {
  id:         field.uuid().primary(),
  full_name:  field.string().required(),
  email:      field.string().required().unique(),
  created_at: field.timestamp().defaultNow(),
});

export type User = InferModel<typeof UsersTable>;
export const UserModel = defineModel(UsersTable);

// ── Policies ───────────────────────────────────────────────────────────────
export const ReadUsersPolicy = policy
  .select('read users')
  .on(UsersTable)
  .to('anon', 'authenticated')
  .using('true')
  .build();

export const InsertUsersPolicy = policy
  .insert('insert users')
  .on(UsersTable)
  .to('anon', 'authenticated')
  .withCheck('true')
  .build();

2. schema/category.ts

import { defineSchema, defineModel, field, policy, InferModel } from 'ngx-saqly-supabase';

// ── Table ──────────────────────────────────────────────────────────────────
export const CategoriesTable = defineSchema('categories', {
  id:   field.bigint().primaryGenerated(),
  name: field.string().required().unique(),
});

export type Category = InferModel<typeof CategoriesTable>;
export const CategoryModel = defineModel(CategoriesTable);

// ── Policies ───────────────────────────────────────────────────────────────
export const ReadCategoriesPolicy = policy
  .select('read categories')
  .on(CategoriesTable)
  .to('anon', 'authenticated')
  .using('true')
  .build();

export const InsertCategoriesPolicy = policy
  .insert('insert categories')
  .on(CategoriesTable)
  .to('anon', 'authenticated')
  .withCheck('true')
  .build();

3. schema/product.ts

Imports tables and policies from user.ts and category.ts, adds relations and its own policies, then declares both migrations.

import {
  defineSchema, defineModel, field,
  relation, policy, alter,
  InferModel, MigrationDefinition,
} from 'ngx-saqly-supabase';

import { UsersTable, ReadUsersPolicy, InsertUsersPolicy } from './user';
import { CategoriesTable, ReadCategoriesPolicy, InsertCategoriesPolicy } from './category';

// ── Table ──────────────────────────────────────────────────────────────────
export const ProductsTable = defineSchema('products', {
  id:          field.bigint().primaryGenerated(),
  name:        field.string().required(),
  price:       field.integer().required(),
  stock:       field.integer().default('0'),
  user_id:     field.uuid().required(),
  category_id: field.bigint().required(),
  created_at:  field.timestamp().defaultNow(),
});

export type Product = InferModel<typeof ProductsTable>;
export const ProductModel = defineModel(ProductsTable);

// ── Relations ──────────────────────────────────────────────────────────────
export const ProductUserRelation = relation.manyToOne(ProductsTable, UsersTable, {
  localKey: 'user_id', foreignKey: 'id', onDelete: 'cascade', onUpdate: 'cascade',
});

export const ProductCategoryRelation = relation.manyToOne(ProductsTable, CategoriesTable, {
  localKey: 'category_id', foreignKey: 'id', onDelete: 'cascade', onUpdate: 'cascade',
});

// ── Policies ───────────────────────────────────────────────────────────────
const ReadProductsPolicy = policy
  .select('read products')
  .on(ProductsTable).to('anon', 'authenticated').using('true').build();

const InsertProductsPolicy = policy
  .insert('insert products')
  .on(ProductsTable).to('anon', 'authenticated').withCheck('true').build();

const UpdateProductsPolicy = policy
  .update('update products')
  .on(ProductsTable).to('anon', 'authenticated').using('true').withCheck('true').build();

const DeleteProductsPolicy = policy
  .delete('delete products')
  .on(ProductsTable).to('anon', 'authenticated').using('true').build();

// ── Migration 1: initial schema ────────────────────────────────────────────
export const productsMigration: MigrationDefinition = {
  name: 'users-products-categories-schema',
  tables:    [UsersTable, CategoriesTable, ProductsTable],
  relations: [ProductUserRelation, ProductCategoryRelation],
  rls:       [UsersTable, CategoriesTable, ProductsTable],
  policies:  [
    ReadUsersPolicy,      InsertUsersPolicy,
    ReadCategoriesPolicy, InsertCategoriesPolicy,
    ReadProductsPolicy,   InsertProductsPolicy,
    UpdateProductsPolicy, DeleteProductsPolicy,
  ],
};

// ── Migration 2: add a column (no SQL needed) ──────────────────────────────
export const addDescriptionMigration: MigrationDefinition = {
  name: 'add-description-to-products',
  alterations: [
    alter(ProductsTable).addColumn('description', field.string()),
  ],
};

4. app.config.ts

import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideSaqlySupabase } from 'ngx-saqly-supabase';
import { environment } from '../environments/environment';
import { productsMigration, addDescriptionMigration } from './schema/product';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),
    provideSaqlySupabase({
      url: environment.supabaseUrl,
      key: environment.supabaseAnonKey,
      migrations: [productsMigration, addDescriptionMigration],
    }),
  ],
};

5. products/admin-migration.component.ts

Same component as the products example — just update ALL_MIGRATIONS to include all your migrations. Keep this list in sync with app.config.ts.

import { Component, OnInit, signal } from '@angular/core';
import { injectMigration, MigrationRunner, MigrationRunResult } from 'ngx-saqly-supabase';
import { productsMigration, addDescriptionMigration } from '../schema/product';

// Keep in sync with app.config.ts migrations array
const ALL_MIGRATIONS = [productsMigration, addDescriptionMigration];

@Component({
  selector: 'app-admin-migration',
  standalone: true,
  template: `
    <section style="padding: 24px; max-width: 860px; margin: auto; font-family: sans-serif;">

      @if (setupReady() === false) {
        <div style="border: 1px solid #f90; border-radius: 8px; padding: 16px; margin-bottom: 24px; background: #fffbe6;">
          <strong>⚠ One-time setup required</strong>
          <p style="margin: 8px 0 12px;">
            Run this SQL once in your
            <a href="https://supabase.com/dashboard" target="_blank">Supabase SQL Editor</a>.
          </p>
          <pre style="background: #111; color: #0f0; padding: 16px; border-radius: 6px; overflow: auto; font-size: 13px;">{{ setupSql }}</pre>
          <button (click)="copySetupSql()">{{ copied() ? '✓ Copied!' : 'Copy SQL' }}</button>
          <button (click)="recheckSetup()" style="margin-left: 8px;">I've run it — re-check</button>
        </div>
      }

      @if (setupReady() === true) {
        <p style="color: green; margin-bottom: 16px;">✓ exec_sql is ready</p>
      }

      @if (setupReady() !== null) {
        <div style="border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 16px;">
          <span style="font-size: 12px; background: #eee; border-radius: 4px; padding: 2px 8px;">Migrations</span>
          <h2 style="margin: 8px 0 4px;">Status</h2>

          @if (loading()) {
            <p style="color: #888; margin-top: 12px;">Checking migrations…</p>
          }

          @if (!loading() && pendingCount() === 0) {
            <p style="color: green; margin-top: 12px;">✓ All migrations are up to date.</p>
          }

          @if (!loading() && pendingCount() > 0) {
            <p style="margin: 12px 0 8px; color: #555;">
              {{ pendingCount() }} pending migration(s) — copy the SQL below and run it in Supabase SQL Editor.
            </p>
            <pre style="background: #111; color: #0f0; padding: 16px; border-radius: 6px; overflow: auto; font-size: 13px;">{{ sql() }}</pre>
          }
        </div>
      }

      @if (runResults().length) {
        <div style="border: 1px solid #ddd; border-radius: 8px; padding: 16px;">
          <h3 style="margin: 0 0 12px;">Run Results</h3>
          @for (r of runResults(); track r.name) {
            <div style="margin-bottom: 8px;">
              @if (r.result.success) {
                <span style="color: green;">✓ {{ r.name }}</span>
              } @else {
                <span style="color: red;">✗ {{ r.name }} — {{ r.result.error }}</span>
              }
            </div>
          }
        </div>
      }

    </section>
  `,
})
export class AdminMigrationComponent implements OnInit {
  private readonly migration = injectMigration();

  readonly setupSql = MigrationRunner.setupSql();
  setupReady   = signal<boolean | null>(null);
  copied       = signal(false);
  loading      = signal(true);
  sql          = signal('');
  pendingCount = signal(0);
  runResults   = signal<MigrationRunResult[]>([]);

  async ngOnInit(): Promise<void> {
    this.setupReady.set(await this.migration.checkSetup());
    await this.loadPending();
  }

  async recheckSetup(): Promise<void> {
    this.setupReady.set(null);
    this.setupReady.set(await this.migration.checkSetup());
    await this.loadPending();
  }

  copySetupSql(): void {
    navigator.clipboard.writeText(this.setupSql).then(() => {
      this.copied.set(true);
      setTimeout(() => this.copied.set(false), 2000);
    });
  }

  private async loadPending(): Promise<void> {
    this.loading.set(true);
    const pending = await this.migration.getPending(ALL_MIGRATIONS);
    this.pendingCount.set(pending.length);
    this.sql.set(await this.migration.previewMany(ALL_MIGRATIONS));
    this.loading.set(false);
  }
}

6. products/products.component.ts

Fetches products joined with their category and owner in a single query.

import { Component, OnInit, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { injectModel } from 'ngx-saqly-supabase';
import { Product, ProductModel } from '../schema/product';

interface ProductWithRelations {
  id:          number;
  name:        string;
  price:       number;
  stock:       number;
  description: string | undefined;
  created_at:  string;
  category:    { id: number; name: string } | null;
  owner:       { id: string; full_name: string } | null;
}

@Component({
  selector: 'app-products',
  standalone: true,
  imports: [FormsModule],
  template: `
    <section style="padding: 24px; max-width: 860px; margin: auto; font-family: sans-serif;">
      <h1>Products</h1>

      @if (message()) { <p>{{ message() }}</p> }

      <div style="display: grid; gap: 12px; margin-bottom: 24px;">
        <input type="text"   placeholder="Name"        [(ngModel)]="name" />
        <input type="number" placeholder="Price"       [(ngModel)]="price" />
        <input type="number" placeholder="Stock"       [(ngModel)]="stock" />
        <input type="text"   placeholder="User ID"     [(ngModel)]="userId" />
        <input type="number" placeholder="Category ID" [(ngModel)]="categoryId" />
        <textarea            placeholder="Description" [(ngModel)]="description"></textarea>
        <button (click)="addProduct()">Add Product</button>
        <button (click)="loadProducts()">Reload</button>
      </div>

      <h3>Total: {{ total() }}</h3>

      @for (p of products(); track p.id) {
        <div style="border: 1px solid #ddd; padding: 16px; margin-bottom: 12px; border-radius: 8px;">
          <strong>{{ p.name }}</strong>
          <p>Price: {{ p.price }} · Stock: {{ p.stock }}</p>
          <p>Category: {{ p.category?.name ?? '—' }}</p>
          <p>Owner: {{ p.owner?.full_name ?? '—' }}</p>
          <p>Description: {{ p.description ?? 'No description' }}</p>
          <button (click)="updateProduct(p)">+10 Price</button>
          <button (click)="deleteProduct(p.id)" style="margin-left: 8px;">Delete</button>
        </div>
      }

      @if (products().length === 0) { <p>No products found.</p> }
    </section>
  `,
})
export class ProductsComponent implements OnInit {
  private readonly repo = injectModel(ProductModel);

  products = signal<ProductWithRelations[]>([]);
  total    = signal(0);
  message  = signal('');

  name        = '';
  price       = 0;
  stock       = 0;
  userId      = '';
  categoryId  = 0;
  description = '';

  async ngOnInit(): Promise<void> {
    await this.loadProducts();
    this.total.set(await this.repo.count());
  }

  async loadProducts(): Promise<void> {
    try {
      const data = await this.repo.findMany({
        select: 'id, name, price, stock, description, created_at, category:categories(id, name), owner:users(id, full_name)',
        orderBy: 'created_at',
        ascending: false,
      });
      this.products.set(data as ProductWithRelations[]);
    } catch {
      this.message.set('Error loading products');
    }
  }

  async addProduct(): Promise<void> {
    if (!this.name.trim())    { this.message.set('Name is required'); return; }
    if (this.price <= 0)      { this.message.set('Price must be > 0'); return; }
    if (!this.userId.trim())  { this.message.set('User ID is required'); return; }
    if (this.categoryId <= 0) { this.message.set('Category ID is required'); return; }
    try {
      await this.repo.insert({
        name:        this.name.trim(),
        price:       this.price,
        stock:       this.stock,
        user_id:     this.userId.trim(),
        category_id: this.categoryId,
        description: this.description.trim() || undefined,
      });
      this.name = ''; this.price = 0; this.stock = 0;
      this.userId = ''; this.categoryId = 0; this.description = '';
      await this.loadProducts();
      this.total.set(await this.repo.count());
      this.message.set('Product added');
    } catch {
      this.message.set('Error adding product');
    }
  }

  async updateProduct(product: Product): Promise<void> {
    try {
      await this.repo.updateById(product.id, { price: product.price + 10 });
      await this.loadProducts();
      this.message.set('Product updated');
    } catch {
      this.message.set('Error updating product');
    }
  }

  async deleteProduct(id: number): Promise<void> {
    try {
      await this.repo.deleteById(id);
      await this.loadProducts();
      this.total.set(await this.repo.count());
      this.message.set('Product deleted');
    } catch {
      this.message.set('Error deleting product');
    }
  }
}

7. app.component.ts

import { Component } from '@angular/core';
import { AdminMigrationComponent } from './products/admin-migration.component';
import { ProductsComponent } from './products/products.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AdminMigrationComponent, ProductsComponent],
  template: `
    <app-admin-migration />
    <app-products />
  `,
})
export class App {}

Query — filter by category

const data = await this.repo.findMany({
  where:  { category_id: 3 },
  select: 'id, name, price, category:categories(id, name)',
});

API Reference

Providers & Tokens

| Export | Description | |---|---| | provideSaqlySupabase(config) | Configure the library — call once in app.config.ts | | SAQLY_SUPABASE_CLIENT | Injection token for the regular Supabase client | | SAQLY_SUPABASE_ADMIN_CLIENT | Injection token for the admin client (serviceRoleKey) | | SAQLY_SUPABASE_CONFIG | Injection token for the resolved config |

SaqlySupabaseConfig

| Property | Type | Required | Description | |---|---|---|---| | url | string | ✓ | Supabase project URL | | key | string | ✓ | Public anon key | | serviceRoleKey | string | — | Service role key — bypasses RLS | | managementKey | string | — | Personal access token — Supabase Management API | | migrations | MigrationDefinition[] | — | Auto-run on app startup |

Schema

| Export | Description | |---|---| | defineSchema(table, fields) | Define a table and return a typed schema | | field | Field factory: field.string(), field.uuid(), … | | InferModel<TSchema> | Derive TypeScript model type from a schema | | alter(schema) | Returns an AlterTableBuilder for generating ALTER TABLE statements | | AlterationDefinition | Type produced by alter(table).addColumn / dropColumn / renameColumn |

Models & CRUD

| Export | Description | |---|---| | defineModel(schema) | Create a model with inferred type | | defineModel<T>(table, options?) | Create a model with explicit type | | injectModel(model) | Inject a ModelRepository<T> in a component | | ModelRepository<T> | findAll · findMany · findFirst · findById · insert · insertMany · upsert · updateById · updateMany · deleteById · deleteMany · count · exists | | WritePayload<T> | { [K in keyof T]?: T[K] \| null } — flat optional type accepted by all write methods |

Relations

| Export | Description | |---|---| | relation.oneToOne(source, target, options) | 1:1 FK constraint | | relation.oneToMany(source, target, options) | 1:M definition | | relation.manyToOne(source, target, options) | M:1 FK constraint — use both sides for M:M |

Migrations

| Export | Description | |---|---| | injectMigration() | Inject a MigrationRunner in a component | | MigrationRunner.preview(def) | Returns full SQL string for one migration — no network call | | MigrationRunner.previewMany(defs) | Returns SQL for pending migrations only — async | | MigrationRunner.run(def) | Executes one migration and marks it applied | | MigrationRunner.runMany(defs) | Runs all pending migrations in order, stops on first failure | | MigrationRunner.getPending(defs) | Returns definitions not yet recorded in _saqly_migrations | | MigrationRunner.download(def) | Downloads .sql file — no network call | | MigrationRunner.checkSetup() | Checks if exec_sql function exists in Supabase | | MigrationRunner.setupSql() | Static — returns one-time bootstrap SQL (exec_sql + tracking table) | | MigrationResult | { sql, success, error?, setupRequired? } | | MigrationRunResult | { name, result: MigrationResult } — returned by runMany() | | generateMigrationSql(def) | Standalone SQL generator | | generateMigrationFileName(name) | Returns YYYYMMDDHHMMSS_name.sql |

Migration tracking: after each successful run(), the migration name is recorded in a _saqly_migrations table in your Supabase project. runMany() and getPending() query this table to skip already-applied migrations automatically. The tracking table is created automatically the first time a migration runs (via exec_sql).

Idempotency: every generated policy statement is preceded by DROP POLICY IF EXISTS, so running the same migration SQL multiple times — or after running it manually in SQL Editor — never fails.

Policies

| Export | Description | |---|---| | policy.select(name) | SELECT policy builder | | policy.insert(name) | INSERT policy builder | | policy.update(name) | UPDATE policy builder | | policy.delete(name) | DELETE policy builder | | policy.all(name) | ALL-operations policy builder | | .on(table) | Target table | | .to(...roles) | 'anon', 'authenticated', or custom | | .using(expr) | Read condition (SELECT / UPDATE / DELETE) | | .withCheck(expr) | Write condition (INSERT / UPDATE) | | .build() | Returns PolicyDefinition |

Auth

| Export | Description | |---|---| | injectAuth() | Inject AuthService | | auth.user() | Signal — User \| null | | auth.loading() | Signal — boolean | | auth.signUp(email, password) | Returns AuthResult | | auth.signIn(email, password) | Returns AuthResult | | auth.signOut() | Signs out |


License

MIT