Show HN: ElectricSQL, Postgres to SQLite active-active sync for local-first apps (electric-sql.com)

617 points by samwillis ↗ HN
Hi HN, James, Valter, Sam and the team from ElectricSQL here.

We're really excited to be sharing ElectricSQL with you today. It's an open source, local-first sync layer that can be used to build reactive, realtime, offline-capable apps directly on Postgres with two way active-active sync to SQLite (including with WASM in the browser).

Electric comprises a sync layer (built with Elixir) placed in front of your Postgres database and a type safe client that allows you to bidirectionally sync data from your Postgres to local SQLite databases. This sync is CRDT-based, resilient to conflicting edits from multiple nodes at the same time, and works after being offline for extended periods.

Some good links to get started:

- website: https://electric-sql.com

- docs: https://electric-sql.com/docs

- code: https://github.com/electric-sql/electric

- introducing post: https://electric-sql.com/blog/2023/09/20/introducing-electri...

You can also see some demo applications:

- Linear clone: https://linear-lite.electric-sql.com

- Realtime demo: https://electric-sql.com/docs/intro/multi-user

- Conflict-free offline: https://electric-sql.com/docs/intro/offline

The Electric team actually includes two of the inventors of CRDTs, Marc Shapiro and Nuno Preguiça, and a number of their collaborators who've pioneered a lot of tech underpinning local-first software. We are privileged to be building on their research and delighted to be surfacing so much work in a product you can now try out.

172 comments

[ 3.7 ms ] story [ 342 ms ] thread
I am extremely, extremely excited about this.

I was wondering: what is the difference between VLCN and ElectricSQL?

Thank you!

Hey,

James here, one of the co-founders of Electric.

Both projects are doing active-active CRDT-based sync. Our focus is on sync via Postgres and on compatibility with existing Postgres-backed applications. So you can drop Electric onto an existing Postgres-backed system and it works with your existing data model.

There's also quite a lot of difference in the development model, how we handle migrations, shape-based partial replication, etc. And we're not focused on p2p sync -- for us, everything goes through Postgres.

Hope that helps -- you can read a bit more about the system design here: https://electric-sql.com/docs/reference/architecture

Thank you for the help! It is quite useful. Another question while I still have you:

On the clientside, is SQLite running in a separate thread? Or is it running on the main thread? Is this the same or different for an electron app vs running in the browser?

With wa-sqlite in IndexedDB mode we're main thread. In OPFS mode it's a worker thread. With the mobile drivers you go over a native bridge.

With Electron / Tauri it's the same as the browser. You're running in Chromium / native WebView on the front-end side.

How do you deal with schema migrations, and software updates (SQLite or Postgres)?
I’m going to have to check this out.

I still maintain that we need an interchange standard for write ahead logs, but tools like this are a step in the right direction.

I would like to point out thought why you may be underselling this. Local-first yes, but that dynamic can also apply to edge-networked apps (at least before everyone started equating edge networking with Lambda).

This has long been a dream data stack of mine. Best of luck.
Can you talk about the dataset sizes you support? Your example linear app has 112 rows, could you support 1000? 10K? 100k? 1M?
OP here, I work for Electric,

Electric is designed to support partial sync, and so you don’t have to sync your whole dataset. (Note that this is feature is under development and not yet public)

There are limitations on how much data a browser will store for an individual site, so the number of rows you can sync will depend on the shape of your dataset. Finally there are also some performance considerations with WASM SQLite, this is something the SQLite team are working on in collaboration with the browser developers, particularly with the development of the new OPFS apis which we plan to support as they mature.

So, thousands of rows are definitely viable, and we have that working with our own internal development tests. Hundreds of thousands or millions may cause issues right now, but are something we do want to support.

I wish they didn’t remove sqlite from browsers “because it’s not an open standard”. It was one very important thing in common between mobile and web, and now it’s clunky and slow on web when running over WASM. I don’t see how that was in any way good for users.
Yeah but SQLite doesn't protect against unlimited CPU use and has historically had issues where "malicious queries" cause crashes -- not OK for a browser to surface to scripts.

https://www.sqlite.org/cves.html

> Almost all CVEs written against SQLite require the ability to inject and run arbitrary SQL.

> The advertised consequence of most CVEs is "denial of service", typically by causing a crash through a NULL pointer dereference or a division by zero, or similar.

> But if an attacker can already run arbitrary SQL, they do not need a bug to cause a denial of service. There are plenty of perfectly legal and valid SQL statements that will consume unlimited CPU, memory, and disk I/O in order to create a denial-of-service without requiring help from bugs.

> Hence, the mere fact that an attacker has a way to inject and run arbitrary SQL is in and of itself a denial-of-service attack. That the arbitrary SQL might also tickle a bug in SQLite and cause a crash is not a new vulnerability.

JavaScript can do that too. Web browsers defend against DoS attacks in the browser by popping up an alert asking if you want to kill the tab process or not. I don’t see why sqlite can’t be run under that model as well.
You’re saying you have partial sync already working for browser sized datasets say 5k rows which are an arbitrary subset of server records which are much larger? What limitations are there on the query criteria for client-synced records, can the subset be expressed as an arbitrary join? How do you implement local first on arbitrary join?
Relevant — PowerSync [1] (similar to Electric; disclosure: co-founder) is currently designed to efficiently sync around 1M rows, or roughly 1GB of uncompressed data. More would be possible, but performance may degrade. In the future we plan on pushing it to handle larger data volumes.

[1] https://docs.powersync.co/

EletricSQL is very cool, congratulations and thanks for the work!

I was thinking about using it for an app that I'm developing but decided to not ship a 2mb binary to every user.

And that led me to discover that IndexedDB is actually quite interesting.

Any plans to integrate EletricSQL with IndexedDB?

Hey, thank you :)

We currently use IndexedDB for the virtual filesystem underneath the wa-sqlite [1] WASM SQLite driver in the browser. But yup, that means a ~1.1MB WASM download.

We'll definitely stay with SQLite in the client. One of our key principles is full SQL support (any Postgres supported SQL on the server, any SQLite supported SQL on the client). So we won't go pure IndexedDB without a database on top.

There's a lot of cool stuff going on with OPFS and WASM SQLite atm, so the technical options are evolving fast.

[1] https://github.com/rhashimoto/wa-sqlite

Is there any validation or authorization for changes being merged back into the root database?

In a traditional client / server model, the server has an opportunity to validate each request and optionally reject it. The lower level you go with the sync protocol (data changes vs high level requests) the more difficult that becomes.

Have you addressed that and, if so, how? What prevents a malicious client from send arbitrary data streams to get synced into the root database?

Not the author, but PostgreSQL has constraint triggers that can run procedures / functions on insert/update/delete, to allow/reject a given row or statement. That would be one way to confirm that a given update from a client is valid, from the POV of the application.
The situation I’m considering is data that matches the referential integrity and check constraints of the database, but is malicious. For example syncing a “salary update to $1M” for yourself into the source database.
Yup, this is what's addressed by write permissions. You can express who can set salaries and column level rules to validate input values.

When it comes to concurrency problems like not spending money twice, the plan is https://electric-sql.com/blog/2022/05/03/introducing-rich-cr...

Row-level permissions also very useful - e.g. I am allowed to update my own profile, but not someone else's.
Hey,

You can see our database rules spec here: https://electric-sql.com/docs/api/ddlx

We haven't implemented it all yet but you can see the intention / direction. It's similar to RLS but adapted for the context.

Connections are authenticated by signed JWT: https://electric-sql.com/docs/usage/auth

We also auto-generate a type-safe data access client from the electrified subset of the Postgres schema. This applies type-level write validation and will apply general write validation when we get there.

James.

> It's similar to RLS but adapted for the context.

I'm visiting the docs.

> ...This must have at least a user_id claim, which should be a non-empty string matching the primary key UUID of the authenticated user...

You should probably strike this from your docs. It sounds like you are still figuring out how this should work.

The "right" answer is to author an OIDC authentication module for Postgres and license it in a way that Amazon will steal it for the purposes of actual adoption; or try to add it to PGBouncer as a proxy, converting JWTs in the password field into "set local user.id = ..." and similar in a preamble for every sql statement.

Projects like postgrest have been down this journey and spell out their solutions. It's all a little vague with Supabase, there isn't anything magical about their authorization approach either, but I wouldn't keeping looking to them as the gold standard implementation for web backend stuff.

Anyway, none of this exists in SQLite, which is a huge pain in the butt. SQLite looks promising, and then it's missing all this stuff, because it doesn't make any sense for SQLite to have row level security or JWTs or whatever. That's the issue isn't it? Like why not step back and say, "is SQLite right for me, if it's not only missing all these features I need, but also they will never be added to SQLite because it doesn't make sense for SQLite to have them?"

Separately, when I visit https://electric-sql.com/docs/intro/local-first, it's ironic, the local first says its latency is 3ms, but because it had to load all this code and stuff, it took longer than 237ms of the "cloud-first" box for that 3ms number to even appear. I've been here before, I was a Meteor developer, I am cognizant of the tradeoffs and what this measurement is saying. There's no such thing as a free lunch.

> Projects like postgrest have been down this journey and spell out their solutions. It's all a little vague with Supabase

Just in case it's not entirely clear: supabase is just PostgreSQL + PostgREST. We contribute to + maintain PostgREST, so if it works with PostgREST it also works with Supabase.

> there isn't anything magical about their authorization approach either

I 100% agree with this, and that's intentional. We don't want to do anything special here, we want our solutions to be as interoperable as possible with existing approaches

I understand it'd be necessary to implement all auth and check rules in SQL queries using roles and the CHECK statement.

What's the alternative in cases where I need more advanced checking before doing an INSERT/UPDATE that is not possible in SQL?

This is usually done in the backend. The frontend is not a trusted environment.

So, I'd just send a request to the backend, perform the checks, modify the data in Postgres and then it'd sync to the clients?

The route that we went with for PowerSync (disclosure: co-founder) is to allow to define your own function for handling writes, where you would use your (presumably existing) backend application API to persist the writes to Postgres. Therefore you can handle any validation/authZ/business logic for writes in your backend layer.

The PowerSync client SDK still handles the queueing and retrying of writes so that they can be automatically retried if network connectivity is not available — whenever there is a retry, your callback function is called. (As a result of this approach, your write function should be idempotent; we commend using GUIDs or UUIDs generate client-side for primary keys)

Similar to Electric, PowerSync also uses JWT for auth against your backend.

There are some architecture diagrams explaining this on our docs root here: https://docs.powersync.co/

You are really taking the expression `shameless plug` to another level in this thread, aren't you?
Genuinely excited about this space and it's what I'm focused on full-time so definitely have thoughts to share. I am wary of self-promotion. I do want to contribute things that I feel are relevant to the discussion, since I assume folks would be interested to see different patterns/approaches around local-first/offline-first architecture.
> Genuinely excited about this space

You can only be genuinely excited if you don't need to add a link to your product when talking about it. This one was actually your most interesting reply, I shouldn't have posted about the high amount of mention to your own competitor project here.

Can you please stop posting as much about PowerSync on a Show HN thread centered on ElectricSQL?
Awesome! I'm looking forward to trying this out. Currently I get this functionality by using PouchDB on the client with a CouchDB sever. Then on my API server I have some janky code in a cron job to sync changes from CouchDB to PostgreSQL.
Hey, I work at Electric,

The CouchDB/PouchDB pattern is how I originally got interested in local first, they are such a good tool, but having the full power of Postgres and then SQLite on the client, I believe, is a real game change.

Sounds like Electric could be a really good fit for your use case. If want any advice join the Discord and we are happy to help out.

Congrats on the launch!

Looking forward to seeing more people trying out this architecture.

Still hoping we can find some time to collaborate on reactivity, tree-sql, typed-sql or some such other effort in the near future.

Thanks :) and yup, definitely, we were digging into the typed-sql repo just the other day.
Awesome.

I've made quite a bit of progress on incremental view maintenance for SQLite which I'd like to share in the near future if that's a problem you're also dealing with.

Congrats on the launch and interesting project! I have a lot of questions :)

* I am by no means a crdt expert, but from my tinkering I have come to the opinion that the upside of the consistency does come at the cost of ease of understanding how state ends up how it does, which can bleed into user facing issues if how a crdt converges conflicts with intuition. A lot of that seems like an education and design problem and I am curious how you are thinking of talking that? Do you plan on some support for server-first writes for situations where crdt might not fit?

* from the docs it seems like adding electric to a table does a migration to add new columns / new tables, I assume, to support the crdt, but my other knowledge of crdts is that it can be expensive in terms of size, especially for preserving history. I will have to poke at it to learn more, but I do wonder where you think you shouldn't enable electric?

* I am curious at the commercial model? Are you going full cloud service like Supabase/neon? Or just host the elixir component?

I will keep poking, but really really interesting!

Hey, thanks :)

We have a Discord if you fancy chatting at more length! https://discord.electric-sql.com

Re: CRDTs and intuition, yup, there's trade offs. We are working hard to deliver a solid model to code against. So we have finality of local writes and standard relational integrity guarantees. You can read a bit more about some of techniques here https://electric-sql.com/blog/2022/05/03/introducing-rich-cr...

Re: electrify yes, we create a shadow table and some operation log tables etc. So there is some storage amplification but it's of the order of 2x (we use operational based CRDTs and we garbage collect the operation log). We see the fit being with standard OLTP workloads, mainstream relational apps. We don't target high volume data ingestion, etc.

Re: commercial model, we're designed for open source self-host. We see Electric as a drop in sync layer, not as a full stack backend-as-a-service. Lots of options but concentrating on product for now.

Wouldn't making a backend service be better all around? All CRDT data could be stored elsewhere. The service could be easily updated and changed, and without changes the database. Any database could be used. Could have LDAP integration and other services.

I'm sure I don't understand CRDTs. Can they be used with database changes that are not part of the CRDT history? That is, can they be added to an existing database with other clients?

A CRDT is a discrete entity/object that has to follow a set of rules for reads, writes, and storage -- including, but not limited to, rules about how to maintain history. You can certainly mix-and-match CRDTs with other entities in the same database, but you can't like "fold in" arbitrary changes to a CRDT without following the rules.
> you can't like "fold in" arbitrary changes to a CRDT without following the rules.

Thanks. I was thinking a few simple rules, like last outside change wins, or monitoring changes and recording the time. But that wasn't realistic.

A separate service still seems like a big win, but I'll have to assume there are similar reasons for it being tightly tied to Postgres.

Will be keeping an eye on this. Are there plans for Swift/Kotlin clients for native iOS/Android apps?
Hey, yup it's definitely a medium term objective.

The team at SkillDevs are maintaining a Daft/Flutter client at https://github.com/SkillDevs/electric_dart

Plus we have a thread to extract the core client-side replication component to Rust to be able to compile for multiple targets.

Congrats to the team. I think it is particularly impressive (and a bit scary) that DDL changes are also synchronized to the clients automatically!
Are there plans for a true native driver (not JS)? That's kind of a dealbreaker in the same groups that are looking for offline first.
There will be other drivers. We are exploring ways to support a wide range of os/platforms but this is a little way off, for now we are concentrating on the js client.
Can you delve a bit deeper into how WASM works? Specifically, how heavy is the WASM module? Also, is there a specific use-case you've seen where this shines particularly brightly?
Hey, I work for Electric.

The WASM module is just a standard build of SQLite, about 1.1mb - there are many landing pages 10x that size - and something that we think is easily justifiable for the types of apps that people can build with Electric.

In terms of use cases, there are numerous, broad-ranging possibilities - anything from a replacement for GraphQL or a REST API in your existing stack, all the way up to building large collaborative apps. Our intention is that you can build collaborative tools similar to Linear, and we do actually have a demo of this sort of thing (http://linear-lite.electric-sql.com), but it’s certainly not limited to that.

You can get that size down to 600KB using brotli compression fwiw. Maybe even a bit smaller using Roy's latest tricks that no longer require asyncify'd builds of SQLite.
I want to be able to have a Postgres database as the central source of truth for all data and user accounts, but then have each users private content to be siloed and synced to their own SQLite database which they alone have access to (maybe even they’re have one SQLite file on server and one SQLite file on phone or etc.). Is this possible with electricSQL? I remember looking at it a year ago or so and was excited but not sure if worked for my use case. Great work tho looks really good and very much in line with what I’d like to be able to do.

Also is there a way to transition an existing Postgres data base into using electicsql?

Hey,

This is the exact pattern we target :)

Drop Electric onto an existing Postgres data model and use the Shape-based sync to manage dynamic partial replication of subsets of data between central Postgres and local SQLites: https://electric-sql.com/docs/usage/data-access/shapes

James

This is awesome. I love the pattern, and the way you’ve built for this as a first-class concern is great.
Congrats on the launch. I’ve been keeping an eye on these sorts of tools for a while and had a look at this after it was mentioned on HN the other day. Looks great.

We currently use Hasura subscriptions to pull data to the frontend (effectively select * from table where account_id = X and updated > recently). We then funnel changed rows into mobx objects so we have a lovely object graph to work with.

I’m imagining doing the same with electric so I guess you’d use notify to hear that there has been a change, then figure out if it’s in a table you care about and then select the updated records from that to merge into mobx?

Basing that off what I see in here: https://github.com/electric-sql/electric/blob/main/clients/t...

Interesting, I don’t know enough about MobX to be sure but yes, you can use the notifier to subscribe to data change notifications and query to get the changed rows.

We’d definitely be interested in chatting through the reactivity model you use if you’d be up for it.

This is also the pattern targeted by PowerSync (disclosure: co-founder) — selectively syncing scoped data for the user from Postgres to their own client SQLite database. Sync Rules are used to define which data is synced to which users: https://docs.powersync.co/usage/sync-rules
This is the fifth comment I’ve counted plugging Powersync in this thread. I guess it’s the nature of the startup hustle game but this level of promotion in someone else’s Show HN thread feels a little distasteful. Maybe you could post your own Show HN?
I hear you, thanks for sharing your thoughts and for the suggestion. Noted. I didn’t mean to just plug — I thought I was contributing to the discussion and that some people may find another implementation relevant/interesting.
Since you are here, I would love to understand what are the conceptual and architectural differences between ElectricSQL and PowerSync.
ElectricSQL stores additional data in the source PostgreSQL database, while PowerSync stores this in a separate data store in its sync service. Because of this, ElectricSQL requires schema changes and has some schema restrictions on the source database.

ElectricSQL uses CRDTs for merging changes, via the Electric backend. PowerSync records changes per transaction on the client and lets the developer write changes to their database, which can be customized with custom write logic, validations, and access control.

ElectricSQL uses “shapes” (currently a WIP feature) defined on the client to partition data, while PowerSync uses server-side “sync rules”. The sync rules approach allows data to be transformed and filtered using basic SQL queries, which we expect to scale well (benchmarks pending). PowerSync guarantees consistency over all synced data, while it appears like ElectricSQL only guarantees consistency within each shape*. PowerSync also automatically adds and removes sync buckets when changes are made to either the sync rules or data, while ElectricSQL likely* requires you to subscribe and remove shapes yourself.

ElectricSQL streams schema migrations to the client. Part of the PowerSync design philosophy is that each client version should have a static schema. PowerSync syncs “schemaless” data, then applies a client-side schema (typically a mirror of the Postgres schema, but can be customized) on top of that using SQLite views.

*We’re making some assumptions about ElectricSQL where the documentation is not clear - please correct us if we got something wrong.

Both these assumptions are incorrect.

We maintain consistency and integrity across shape boundaries.

Thanks for the clarification
You could store a SQLite database in a blob column for the user, though this would be an affront to $DEITY.
One cool feature would be to encrypt local SQL database with the user's key before syncing. The provider of the PostgreSQL database would have zero-access encryption while still having full SQL capabilities on the client.
Oh wow, I’ve been wanting this for a long time, to the point that I started making it myself.

This is perfect to create super fast offline first apps like linear.app, with Postgres

Congrats to the team. I'm on the team over at https://powersync.co and have been following this space for a while.

PowerSync is also a plug-in sync layer. The biggest difference I see is in Electric's use of CRDTs, where we don't rely on them and instead use server reconciliation.

As a team that's been working on online/offline sync for just over a decade, it's great to finally see more products that enable offline-first architectures!

Pretty interesting, what's the benefit of using a check expression rather than row level security?
We do row (and column) level security, the check expression provides an optional additional granularity for controlling which roles can make certain writes
Wow, this is exactly what I've been searching for! I knew this was possible but am too busy trying to build web apps to spend time learning how to do stuff like this.

I notice this is still a 0.X version. How comfortable are you with people using this in production? Are there any success stories so far? And if it's not production ready, is there a roadmap I can check out?

The company I work for is in the process of planning a complete rework of our app and right now is the time for us to choose technologies. I so very much want to use this or something like this.

Hey, very cool :)

We don't recommend production use right now. There's a roadmap page here https://electric-sql.com/docs/reference/roadmap -- we particularly need to flesh out shapes, permissions and validation.

Happy to chat (e.g. on our Discord) if you'd like a bit more detail / looking at whether Electric could fit into your tech pans.

Thanks for the roadmap, I should have just looked around the site a bit more, I would have found it.

And, I may take you up on a chat after I've had a conversation with my team! At the very least, I've joined the Discord and I'll definitely be following you guys closely.

> I so very much want to use this or something like this.

Since you mentioned this — you could also take a look at PowerSync which is similar (disclosure: co-founder) https://docs.powersync.co/ - Currently beta suitable for production use.

Yes, saw your other comment! ;) I will definitely be checking this out as well. I am so glad folks are finally creating real solutions in this space.
How does this scale? For example, can you shard user DB's across multiple PGSQL clusters?
We cover this a bit in the architecture page: https://electric-sql.com/docs/reference/architecture

Basically Electric is horizontally scalable. You can run multiple Electric sync services on top of Postgres and then obviously many clients connecting to the Electric. We're focused on running on top of a single Postgres right now (which we take query load off).

We've been chatting with the ElectricSQL team for a few weeks, super responsive and open to adapting to our use case. We're building an offline-first, mobile-first app and have high hopes for this project!