惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
Google Online Security Blog
Google Online Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Simon Willison's Weblog
Simon Willison's Weblog
T
Threat Research - Cisco Blogs
C
CXSECURITY Database RSS Feed - CXSecurity.com
L
Lohrmann on Cybersecurity
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Forbes - Security
Forbes - Security
P
Palo Alto Networks Blog
Schneier on Security
Schneier on Security
S
Schneier on Security
T
Tor Project blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
WordPress大学
WordPress大学
The Hacker News
The Hacker News
Hacker News - Newest:
Hacker News - Newest: "LLM"
罗磊的独立博客
Application and Cybersecurity Blog
Application and Cybersecurity Blog
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
C
Check Point Blog
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
雷峰网
雷峰网
S
Security @ Cisco Blogs
PCI Perspectives
PCI Perspectives
Spread Privacy
Spread Privacy
W
WeLiveSecurity
SecWiki News
SecWiki News
A
About on SuperTechFans
H
Help Net Security
博客园 - 司徒正美
Recent Commits to openclaw:main
Recent Commits to openclaw:main
爱范儿
爱范儿
S
Securelist
M
MIT News - Artificial intelligence
云风的 BLOG
云风的 BLOG
月光博客
月光博客
Jina AI
Jina AI
博客园 - 叶小钗
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Recent Announcements
Recent Announcements
S
Secure Thoughts
The Cloudflare Blog
美团技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Local-first Realtime Apps with Expo and Legend-State
Jay Meistrich, Thor Schaeff · 2024-09-23 · via Supabase Blog

Local-first Realtime Apps with Expo and Legend-State

Legend-State is a super fast all-in-one state and sync library that lets you write less code to make faster apps. Legend-State has four primary goals:

  1. As easy as possible to use.
  2. The fastest React state library.
  3. Fine-grained reactivity for minimal renders.
  4. Powerful sync and persistence (with Supabase support built in!)

And, to put the cherry on top, it works with Expo and React Native (via React Native Async Storage). This makes it a perfect match for building local-first mobile and web apps.

In local-first software, "the availability of another computer should never prevent you from working" (via Martin Kleppmann). When you are offline, you can still read and write directly from/to a database on your device. You can trust the software to work offline, and you know that when you are connected to the internet, your data will be seamlessly synced and available on any of your devices running the app. When you're online, this architecture is well suited for "multiplayer" apps, as popularized by Figma.

To dig deeper into what local-first is and how it works, refer to the Expo docs.

A primary goal of Legend-State is to make automatic persisting and syncing both easy and very robust, as it's meant to be used to power all storage and sync of complex apps.

Any changes made while offline are persisted between sessions to be retried whenever connected. To do this, the sync system subscribes to changes on an observable, then on change goes through a multi-step flow to ensure that changes are persisted and synced.

  1. Save the pending changes to local persistence.
  2. Save the changes to local persistence.
  3. Save the changes to remote persistence.
  4. On remote save, set any needed changes (like updated_at) back into the observable and local persistence.
  5. Clear the pending changes in local persistence.

To set up a new React Native project you can use the create-expo-app utility. You can create a blank app or choose from different examples.

For this tutorial, go ahead and create a new blank Expo app:


_10

npx create-expo-app@latest --template blank


The main dependencies you need are Legend State and supabase-js. Additionally, to make things work for React Native, you will need React Native Async Storage and react-native-get-random-values (to generate uuids).

Install the required dependencies via expo install:


_10

npx expo install @legendapp/state@beta @supabase/supabase-js react-native-get-random-values @react-native-async-storage/async-storage


If you don't have a Supabase project already, head over to database.new and create a new project.

Next, create a .env.local file in the root of your project and add the following env vars. You can find these in your Supabase dashboard.


_10

EXPO_PUBLIC_SUPABASE_URL=

_10

EXPO_PUBLIC_SUPABASE_ANON_KEY=


Next, set up a utils file to hold all the logic for interacting with Supabase, we'll call it utils/SupaLegend.ts.


_10

import { createClient } from '@supabase/supabase-js'

_10

_10

const supabase = createClient(

_10

process.env.EXPO_PUBLIC_SUPABASE_URL,

_10

process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY

_10

)


Legend-State is very versatile and allows you to choose different persistence and storage strategies. For this example, we'll use React Native Async Storage for local persistence across platforms and supabase for remote persistence.

Extend your utils/SupaLegend.ts file with the following configuration:


_46

import { createClient } from '@supabase/supabase-js'

_46

import { observable } from '@legendapp/state'

_46

import { syncedSupabase } from '@legendapp/state/sync-plugins/supabase'

_46

import { configureSynced } from '@legendapp/state/sync'

_46

import { observablePersistAsyncStorage } from '@legendapp/state/persist-plugins/async-storage'

_46

import AsyncStorage from '@react-native-async-storage/async-storage'

_46

_46

const supabase = createClient(

_46

process.env.EXPO_PUBLIC_SUPABASE_URL,

_46

process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY

_46

)

_46

_46

// Create a configured sync function

_46

const customSynced = configureSynced(syncedSupabase, {

_46

// Use React Native Async Storage

_46

persist: {

_46

plugin: observablePersistAsyncStorage({

_46

AsyncStorage,

_46

}),

_46

},

_46

generateId,

_46

supabase,

_46

changesSince: 'last-sync',

_46

fieldCreatedAt: 'created_at',

_46

fieldUpdatedAt: 'updated_at',

_46

// Optionally enable soft deletes

_46

fieldDeleted: 'deleted',

_46

})

_46

_46

export const todos$ = observable(

_46

customSynced({

_46

supabase,

_46

collection: 'todos',

_46

select: (from) => from.select('id,counter,text,done,created_at,updated_at,deleted'),

_46

actions: ['read', 'create', 'update', 'delete'],

_46

realtime: true,

_46

// Persist data and pending changes locally

_46

persist: {

_46

name: 'todos',

_46

retrySync: true, // Persist pending changes and retry

_46

},

_46

retry: {

_46

infinite: true, // Retry changes with exponential backoff

_46

},

_46

})

_46

)


If you haven't alread, install the Supabase CLI and run supabase init to initialize your project.

Next, create the initial database migration to set up the todos table:


_10

supabase migrations new init


This will create a new SQL migration file in the supabase/migrations directory. Open it and add the following SQL code:


_34

create table todos (

_34

id uuid default gen_random_uuid() primary key,

_34

counter bigint generated by default as identity,

_34

text text,

_34

done boolean default false,

_34

created_at timestamptz default now(),

_34

updated_at timestamptz default now(),

_34

deleted boolean default false -- needed for soft deletes

_34

);

_34

_34

-- Enable realtime

_34

alter

_34

publication supabase_realtime add table todos;

_34

_34

-- Legend-State helper to facilitate "Sync only diffs" (changesSince: 'last-sync') mode

_34

CREATE OR REPLACE FUNCTION handle_times()

_34

RETURNS trigger AS

_34

$$

_34

BEGIN

_34

IF (TG_OP = 'INSERT') THEN

_34

NEW.created_at := now();

_34

NEW.updated_at := now();

_34

ELSEIF (TG_OP = 'UPDATE') THEN

_34

NEW.created_at = OLD.created_at;

_34

NEW.updated_at = now();

_34

END IF;

_34

RETURN NEW;

_34

END;

_34

$$ language plpgsql;

_34

_34

CREATE TRIGGER handle_times

_34

BEFORE INSERT OR UPDATE ON todos

_34

FOR EACH ROW

_34

EXECUTE PROCEDURE handle_times();


The created_at, updated_at, and deleted columns are used by Legend-State to track changes and sync efficiently. The handle_times function is used to automatically set the created_at and updated_at columns when a new row is inserted or an existing row is updated. This allows to efficiently sync only the changes since the last sync.

Next, run supabase link to link your local project to your Supabase project and run supabase db push to apply the init migration to your Supabase database.

Legend-State integrates with supabase-js to provide end-to-end type safety. This means you can use the existing Supabase CLI workflow to generate TypeScript types for your Supabase tables.


_10

supabase start

_10

supabase gen types --lang=typescript --local > utils/database.types.ts


Next, in your utils/SupaLegend.ts file, import the generated types inject them into the Supabase client.


_10

import { createClient } from '@supabase/supabase-js'

_10

import { Database } from './database.types'

_10

// [...]

_10

_10

const supabase = createClient<Database>(

_10

process.env.EXPO_PUBLIC_SUPABASE_URL,

_10

process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY

_10

)

_10

_10

// [...]


From here, Legend-State will automatically infer the types for your Supabase tables and make them available within the observable.

Above, you've configured the todos$ observable. You can now import this in your tsx files to fetch and automatically sync changes.


_12

import { observer } from '@legendapp/state/react'

_12

import { todos$ as _todos$ } from './utils/SupaLegend'

_12

_12

const Todos = observer(({ todos$ }: { todos$: typeof _todos$ }) => {

_12

// Get the todos from the state and subscribe to updates

_12

const todos = todos$.get()

_12

const renderItem = ({ item: todo }: { item: Tables<'todos'> }) => <Todo todo={todo} />

_12

if (todos)

_12

return <FlatList data={Object.values(todos)} renderItem={renderItem} style={styles.todos} />

_12

_12

return <></>

_12

})


observer is the suggested way of consuming observables for the best performance and safety.

It turns the entire component into an observing context - it automatically tracks observables for changes when get() is called, even from within hooks or helper functions.

This means, as long as realtime is enabled on the respective table, the component will automatically update when changes are made to the data!

Also, thanks to the persist and retry settings above, Legend-State will automatically retry to sync changes if the connection is lost.

To add a new todo from the application, you will need to generate a uuid locally to insert it into our todos observable. You can use the uuid package to generate a uuid. For this to work in React Native you will also need the react-native-get-random-values polyfill.

In your SupaLegend.ts file add the following:


_20

// [...]

_20

import 'react-native-get-random-values'

_20

import { v4 as uuidv4 } from 'uuid'

_20

// [...]

_20

_20

// Provide a function to generate ids locally

_20

const generateId = () => uuidv4()

_20

_20

export function addTodo(text: string) {

_20

const id = generateId()

_20

// Add keyed by id to the todos$ observable to trigger a create in Supabase

_20

todos$[id].assign({

_20

id,

_20

text,

_20

})

_20

}

_20

_20

export function toggleDone(id: string) {

_20

todos$[id].done.set((prev) => !prev)

_20

}


Now, in your App.tsx file, you can import the addTodo and toggleDone methods and call them when the user submits a new todo or checks off one:


_46

import { useState } from 'react'

_46

import { FlatList, StyleSheet, Text, TextInput, TouchableOpacity } from 'react-native'

_46

// [...]

_46

import { observer } from '@legendapp/state/react'

_46

import { addTodo, todos$ as _todos$, toggleDone } from './utils/SupaLegend'

_46

// [...]

_46

_46

// Emojis to decorate each todo.

_46

const NOT_DONE_ICON = String.fromCodePoint(0x1f7e0)

_46

const DONE_ICON = String.fromCodePoint(0x2705)

_46

_46

// The text input component to add a new todo.

_46

const NewTodo = () => {

_46

const [text, setText] = useState('')

_46

const handleSubmitEditing = ({ nativeEvent: { text } }) => {

_46

setText('')

_46

addTodo(text)

_46

}

_46

return (

_46

<TextInput

_46

value={text}

_46

onChangeText={(text) => setText(text)}

_46

onSubmitEditing={handleSubmitEditing}

_46

placeholder="What do you want to do today?"

_46

style={styles.input}

_46

/>

_46

)

_46

}

_46

_46

// A single todo component, either 'not done' or 'done': press to toggle.

_46

const Todo = ({ todo }: { todo: Tables<'todos'> }) => {

_46

const handlePress = () => {

_46

toggleDone(todo.id)

_46

}

_46

return (

_46

<TouchableOpacity

_46

key={todo.id}

_46

onPress={handlePress}

_46

style={[styles.todo, todo.done ? styles.done : null]}

_46

>

_46

<Text style={styles.todoText}>

_46

{todo.done ? DONE_ICON : NOT_DONE_ICON} {todo.text}

_46

</Text>

_46

</TouchableOpacity>

_46

)

_46

}


Since Legend-State utilizes supabase-js under the hood, you can use Supabase Auth and row level security to restrict access to the data.

For a tutorial on how to add user management to your Expo React Native application, refer to this guide.

Legend-State and Supabase are a powerful combination for building local-first applications. Legend-State pairs nicely with supabase-js, Supabase Auth and Supabase Realtime, allowing you to tap into the full power of the Supabase Stack while building fast and delightful applications that work across web and mobile platforms.

Want to learn more about Legend-State? Refer to their docs and make sure to follow Jay Meistrich on Twitter!