# PowerSync — full text Every blog post and legal page on https://powersync.com, concatenated. Individual documents are served as markdown at the same URL with a `.md` suffix (for example https://powersync.com/blog/.md). # How smry.ai Fixed Agentic Development Velocity Using PowerSync > smry’s founder built his own sync engine which started feeling like a quagmire for his coding agents. Shipping code changes was slow and brittle. He replaced it with PowerSync. - Published: 2026-10-19 - Author: Kobie Botha - Category: Showcase - Source: https://powersync.com/blog/how-smry-replaced-its-homegrown-sync-engine-with-powersync --- Michael is the founder of [smry](http://smry.ai), an all-in-one clean reader, read-it-later and archiving tool with >300k MAUs. Michael was inspired by Linear’s UX, and built smry with instant reactivity at the core of the app. To achieve this, he cobbled together his own sync engine using IndexedDB, R2 and Redis. He ran into a bunch of issues with this architecture. In this post, we’ll be describing why and how he switched to PowerSync, and what the outcome was. ## How smry began Michael had one primary requirement for smry’s web UI from day one: everything the user did needed to be instant. Opening of history? Clicking on an article? Navigation? All instant. Additionally, he had a long term vision of a mobile app for smry, which would need to support offline reading mode. It was also important to him that users of smry felt like they owned the content, and the best way to achieve that was to store the data in the browser using IndexedDB, as well as his backend DB (Neon). His initial architecture was quite simple: ![smry’s initial architecture, with custom article sync code connecting IndexedDB and Neon Postgres](https://powersync.com/images/blog/smry/smry-initial-sync-architecture.png) One issue with this architecture was that it wasn’t feasible to sync attachments into Postgres, so R2 was used for attachments on the backend, and `localStorage` on the client: ![smry’s attachment architecture, adding custom sync code between browser storage, Neon Postgres and R2](https://powersync.com/images/blog/smry/smry-attachment-sync-architecture.png) This architecture still didn’t deliver on the instant UI promise though, so Redis was added to speed up DB queries: ![smry’s expanded architecture, with separate article, content cache, and chat, highlights and notes sync code connecting browser storage to Neon Postgres, R2 and Redis](https://powersync.com/images/blog/smry/smry-redis-sync-architecture.png) At this point, everything was miraculously working. There was one snag, however. The system was incredibly complex and hard to reason about, and coding agents would constantly be breaking stuff. In Michael’s own words: > Honestly, Redis was the thing that made this work, because basically Redis was caching everything, and even if something broke, Redis would just cache it and the user would get something in a reasonable amount of time. Much faster than a database call. But it's still not fast enough. Redis isn't literally instant. There's still latency, especially depending on where the user is in the world. A lot of users are not in the US, so they don't have good internet connections. > > The biggest problem for me was development velocity, to be honest. And whenever I'd deploy, I'd be like, "Where are my agents breaking something?” Honestly, the code got really, really messy. > > Also, whenever I would change my database schema, IndexedDB would also be very, very annoying and break, right? Because I was doing database migrations, but IndexedDB doesn't care about your database. So, that was a massive problem. So I knew I needed some sync layer created with a little bit more foresight. ## Michael sends out an SOS ![Michael’s post on X asking for stable IndexedDB-to-Postgres sync that works with his existing database, similar to Linear’s sync engine with less custom code](https://powersync.com/images/blog/smry/smry-michael-sync-sos.png) After a casual chat between Michael and Kobie on X, Michael was intrigued: > My goals for a migration to PowerSync was, hey, massively simpler code, right? The PowerSync FDE team jumped in to show Michael what smry could look like with PowerSync, and the app was migrated to a PowerSync architecture: ![smry’s PowerSync architecture: Neon Postgres syncs through the PowerSync Service to the Web SDK, writes use the existing Next.js app, and built-in attachment management connects to R2](https://powersync.com/images/blog/smry/smry-powersync-architecture.png) We’ll let Michael describe the benefits of this architecture in his own words: > With PowerSync at the core of smry, it made me much more excited about expanding the product. Now efforts are going into a mobile app because, hey, this is now much easier to think about with a mobile app, right? Now that we have PowerSync, it allows for a lot more developer velocity. Now I don't check my sync layer every time I want to push a fix. > > And yes, the biggest thing for me was agents. So smry, a one-person project, I don't have time to learn new PowerSync skills. Agents seem to grok PowerSync. And at this point I can make changes without worrying that an agent's going to f\*\*\* up my DB or f\*\*\* up my sync or f\*\*\* up something else, which is quite nice. And so overall, the biggest unlock with PowerSync was development velocity. Because now I could, like, change things on my app. > > Actually, there was a massive set of migrations that I needed to do, and I was delaying them because of the sync layer problem, because these migrations would make the sync layer just, like, much more complex. And now, with PowerSync, I could actually, like, redesign the app, make migrations, make massive changes. People who aren't me can push code without me worrying that something massive just broke that I have to spend, you know, hours or days fixing. So right now, like, the app went through some major app redesigns, and this was actually, like, pretty much enabled by PowerSync, if I'm being perfectly honest. If you’re considering adding sync to the core of your product, or if you rolled your own sync and any of this resonates - we’d love to [hear from you](https://calendly.com/powersync/powersync-chat). ## App Details
App Name smry
Platforms Web, iOS Extension
Source Database Neon
Backend API Stack Next.js
AI Services via Vercel AI SDK OpenRouter Main model gateway: Remy chat, summaries, explanations, and text-to-speech
Groq Fast inference for article cleanup, translation, quizzes, mind maps, and briefings
External AI Services Context.dev Article extraction, file parsing, web search, page reads, and research
Classifier.dev Article completeness checks, automatic tags, and feed relevance classification
Configure.dev Remy’s personalization, user memory/profile context, and connected tools
Cloudflare Workers AI File/image conversion and audio transcription using Whisper
Pulpie ML-based extraction of an article’s main content from HTML
Pangram AI authorship detection
Client-side Frameworks React
PowerSync React hooks (@powersync/react)
ORMs Drizzle
PowerSync Drizzle Driver (@powersync/drizzle-driver)
Auth Provider Clerk
PowerSync Deployment PowerSync Cloud
--- # How HosTalky Uses PowerSync as the Sync Layer for Its AI-Powered Healthcare Platform > I first met the HosTalky team in March of 2025 when they were looking for a Realm replacement. We’ve had a long working relationship and they’ve helped us make various product improvements during that time. In this post I chat to them about their healthcare platform. - Published: 2026-09-28 - Author: Kobie Botha - Category: Showcase - Source: https://powersync.com/blog/how-hostalky-uses-powersync-ai-healthcare-platform --- I first met the HosTalky team in March of 2025 when they were looking for a Realm replacement. We’ve had a long working relationship and they’ve helped us make various product [improvements](https://github.com/powersync-ja/powersync-js/issues/553) during that time. In this post I chat to them about their healthcare platform. ## Background HosTalky is a clinical workflow intelligence platform for healthcare teams: secure one-to-one and group messaging between clinicians, organization-wide announcements, shared reminders and tasks, personal notes, and CareID, a verifiable digital identity for healthcare professionals. Its core feature is an AI-assisted clinical scribe that turns a spoken clinical encounter into a structured clinical note in formats such as SOAP, H&P, Physiotherapy, DAP and BIRP; the same pipeline can also produce meeting summaries, follow-ups and results reviews. The apps run on iOS, Android and desktop, and HosTalky has been accepted into a leading U.S. health-system accelerator program. When the team first mapped every vendor that stores or transmits PHI, the obvious subsystems were considered - transcription, the LLM, cloud hosting and the database, but the sync layer wasn’t part of the list: “Candidly, PowerSync was not on the first version of that map,” the team told us. “We added PowerSync once we recognized that syncing note and message data to client devices means PHI transits, and is processed by, the sync service — which makes it a business associate.” In this interview, HosTalky’s engineers explain how they closed that gap with a Business Associate Agreement (BAA) and a HIPAA compliant configuration, and why they deliver AI-generated notes to devices through sync rather than polling. Two things make HosTalky an attractive PowerSync case study. The first is that the team uses sync as the delivery channel for AI output: when a clinician stops recording, the note is generated asynchronously on the server, and the device learns that it is finished the same way it learns about any other change — the record updates. I call this inference-in-the-loop sync: it’s essentially synced event sourcing. The second is that they had to fit a sync engine into a HIPAA compliance architecture, and they were candid with us about where that was harder than expected. The interview below has been lightly edited for length and clarity. ## App Details | | | | :- | :- | | **App Name** | HosTalky | | **Platforms** | iOS, Android, Desktop | | **Source database** | MongoDB Atlas | | **Bucket storage database** | MongoDB Atlas | | **Backend API Stack** | GraphQL | | **AI services** | Deepgram (real-time speech-to-text), OpenAI (note generation) | | **Client-side frameworks** | Mobile: React Native (`@powersync/react-native`)
Desktop: Electron (`@powersync/web`)
PowerSync React hooks (`@powersync/react`) | | **ORMs** | Drizzle | | **Auth Provider** | AWS Cognito | | **PowerSync Deployment** | PowerSync Cloud, HIPAA-compliant configuration with BAA | ## Interview ### The AI scribe **PowerSync** Walk us through the AI note feature end to end: a clinician taps record, and then what happens? What runs on-device vs. server-side, and how do Deepgram + OpenAI fit in? **HosTalky** A clinician taps record, and audio is captured on-device and streamed live to a dedicated WebSocket transcription service, which forwards it to Deepgram's real-time speech-to-text API. Deepgram streams back live captions during the recording, so the clinician sees text appear as they talk. When they stop, the client sends the accumulated transcript to our backend, which queues an async job and calls OpenAI to turn that transcript into a structured clinical note. The note updates in place once generation finishes. In short: Deepgram handles live transcription in real time; OpenAI handles note generation afterward, asynchronously, server-side. The device captures the audio and displays the captions as Deepgram streams them back; the speech-to-text itself and the note generation both happen server-side. ![HosTalky’s AI scribe pipeline: live transcription during recording, asynchronous note generation when the clinician stops, and delivery of the finished note to every device via PowerSync](https://powersync.com/images/blog/how-hostalky-uses-powersync-ai-healthcare-platform-inline-architecture.png) **PowerSync** Is one note backed by multiple recordings (for example, a clinician adding to the same note across a shift)? **HosTalky** Yes. Each recording pass is its own session, and a single note can be backed by several of them. If a clinician records again later in the same shift, the new session is appended rather than overwriting what came before, and the note is regenerated from the full history each time. **PowerSync** Do you keep the verbatim transcript or only the generated note? **HosTalky** We keep both, so the clinician can check the transcript if they have any doubt about the note. We keep the verbatim, speaker-labeled transcript on the note indefinitely, alongside the generated note, with no automatic expiry. The raw audio itself is not persisted on our side: it is used momentarily for transcription and then discarded. **PowerSync** Does the app treat an AI-generated note differently from one a clinician typed by hand? **HosTalky** Yes, and whether a note was AI-generated is recorded on the note itself rather than inferred. It drives the client UI: the transcript tab, and on desktop a confirmation before recording over a manually typed note. Server-side it determines how a note update is processed, since a manually typed note is handled differently from an AI-generated one being regenerated. **PowerSync** What are clinicians actually dictating - patient handovers, shift notes, personal reminders? Does any of it contain PHI? **HosTalky** The substance of a clinical encounter - patient history, conditions, medications, assessment and plan - so yes, it routinely contains PHI. That is precisely why the scribe path is treated as an in-scope PHI flow end to end. **PowerSync** Is AI anywhere in the messaging path, or only in notes? **HosTalky** Only in the notes and scribe path; clinician messaging is a separate path with no AI in it. We do have a separate AI assistant in the app, Medigenie, but that is its own feature rather than something layered onto chat. ### Sync as the delivery channel for AI output **PowerSync** When a note is being generated, the client doesn’t poll an endpoint for status, the generating state is stored on the note and syncs to the device like any other data. Was that a deliberate architectural choice, and what did it get you? **HosTalky** Yes, deliberate. Generation status lives as a normal field on the note record and syncs down like any other data, rather than the client polling an endpoint for status. That gets us several things at once with no extra plumbing. The “generating” state survives an app restart, because it is just persisted data, not in-memory state tied to a live request. It works across devices automatically, since notes are scoped to the user rather than the device — if a note is generated on one device, any other device the clinician is logged into sees the same status update. And it needed no separate polling loop, retry logic, or push notification system, since PowerSync already handles keeping the client in sync. **PowerSync** Did you build it that way from the start, or did you begin with request/response and move to sync after something broke? **HosTalky** It started using synchronous API calls. Notes were originally generated through the same blocking request/response endpoint as everything else, so the clinician’s screen was stuck waiting until generation finished. For longer transcripts that wait got noticeably worse, since generation time scales with transcript length. That is what pushed us to move note generation onto an async job with status delivered via sync, so the client isn’t blocked waiting on the response and can move on while the note finishes generating in the background. **PowerSync** Do you stream partial output into the note as tokens arrive, or write the whole note once generation completes? **HosTalky** The whole note once generation completes — no token-by-token streaming. The generation step produces the full note text, then a single write updates the note’s content and flips its generating flag to false in one update. The client sees the finished note appear once, rather than watching it build up piece by piece. **PowerSync** What happens when generation fails or times out? **HosTalky** Generation is retried automatically up to three times; after that the job moves to a dead-letter queue and we get an internal alert. There was a real gap there, and we have a fix in review: originally no error or failure state was written back to the note, so if all retries were exhausted a note could keep showing “generating”. Sync only tells the client when generation finishes, not when it fails. The fix adds proper status and failure handling on the note itself, plus a timeout watchdog as a backstop. _**Editor’s note:** This lesson generalizes: if you deliver job status through sync, model and sync a failure status as data too._ **PowerSync** What does the experience look like when a clinician records in a dead spot — cold storage, basement radiology, an ambulance? **HosTalky** Recording requires a live connection today, since audio streams to our speech-to-text service in real time rather than being saved locally first. If connectivity drops partway through, we stop the recording and generate a note from what was transcribed up to that point, with a clear error and restart prompt for the clinician. Fully offline recording (capturing audio locally and generating once connectivity returns) isn’t there yet. We are looking into ways to close that gap, including queuing the recording locally for processing once back online, and on-device transcription models so recording wouldn’t need a network connection at all. **PowerSync** Roughly how much of this would you have had to build yourselves if PowerSync weren’t in the loop? Things like a job queue, status polling, reconnect, dedupe logic, etc. **HosTalky** A fair amount. The AI job processing itself, namely the queue, workers, retries and dead-lettering, is something we built regardless since that is our own AI pipeline, not something PowerSync does. What PowerSync replaces is everything on the delivery side: a polling endpoint the client would hit repeatedly to check whether a note is ready, backoff and retry logic for that polling, reconnect handling so polling resumes correctly after a network drop, and deduping so the client doesn’t re-show a completion notification it already handled. Multi-device consistency would also be extra work, since we would need to make sure every device a clinician is logged into learns that a note finished, not just the one that requested it. With PowerSync, all of that collapses into watching a local database table for changes, since the sync engine already handles the connection, retry and delivery guarantees underneath it. ### HIPAA and the compliance architecture **PowerSync** You have BAAs in place with Deepgram, OpenAI and MongoDB. How did you approach mapping which vendors touch PHI, and where did a sync engine fit in that map? Did you initially think of PowerSync as a data processor at all? **HosTalky** We built a vendor-by-vendor map of every service that creates, receives, stores or transmits PHI, and required either a BAA or a design that keeps PHI out of scope for each one. Candidly, PowerSync was not on the first version of that map. Our initial list focused on the obvious PHI touchpoints: transcription (Deepgram), the LLM (OpenAI), cloud hosting (AWS) and the database (MongoDB). We added PowerSync once we recognized that syncing note and message data to client devices means PHI transits, and is processed by, the sync service — which makes it a business associate. Closing that gap, with the BAA plus a compliant configuration, is a big part of why this exercise mattered to us. **PowerSync** What has been hardest about assembling HIPAA coverage? The paperwork, the per-vendor plan-tier gates, or the engineering controls? **HosTalky** A mix, but the per-vendor plan-tier gates were the most recurring friction. HIPAA features — BAAs, zero-retention options, dedicated infrastructure — often sit on a higher plan than our actual usage needs, so we repeatedly had to weigh paying for a tier we didn’t technically need against the compliance requirement. The engineering controls were substantial but tractable, because roughly 70% of the safeguards were already covered by our existing ISO 27001/27701-aligned policy set. The paperwork was the least of it. **PowerSync** Under PowerSync’s shared-responsibility model, the client side (the local SQLite database and what happens to it over a device’s lifecycle) is the customer’s to secure. What does that look like in HosTalky? **HosTalky** The mobile apps use SQLCipher, compiled into the native build, with an encryption key that is generated on-device and stored in the OS keychain. On the device lifecycle side, an explicit logout clears the local database through PowerSync’s `disconnectAndClear`, wiping the synced data off the device. The same happens on login and on account switch, so a new user never sees a previous user’s data. **PowerSync** How do you think about HIPAA’s “minimum necessary” standard when deciding what each device syncs? Has asking “what does this device actually need offline?” changed how you scope things? **HosTalky** The design already reflects that thinking. Personal notes, for example, are scoped to their own clinician rather than the whole care team or organization, so a device only ever holds the notes that clinician created, not a shared pool. Ownership-based scoping like that is the default pattern across the app, which lines up well with minimum necessary. _**Editor’s note:** This kind of ownership-based scoping is expressed in PowerSync Sync Streams with a few lines of SQL and enforced server-side, so a device only ever receives the rows its user is entitled to._ **PowerSync** Was anything about PowerSync’s HIPAA shared-responsibility model unclear, or something you’d want documented better? **HosTalky** The split was clear on the big structural pieces — the dedicated Atlas bucket-storage cluster in our own account, the network restrictions, and that client-side controls like local database encryption and device wipe sit with us. Two things we would have valued earlier or more explicitly. First, that HIPAA eligibility — BAA availability — is gated to the Team and Enterprise tiers; surfacing that path, and the requirement to bring your own dedicated bucket-storage cluster, right at the start of scoping would have saved us a cycle. Second, clearer guidance on the client-side expectations under the shared model. The docs are strong on the infrastructure side, but a checklist of what the customer owns on-device — local encryption, logout and wipe behavior, de-provisioning — would help teams self-audit against the model rather than inferring it. _**Editor’s note:** We’ve updated our docs based on this feedback._ ### Reliability, and what could be better **PowerSync** Is HosTalky reliable for your users today? Any sync- or data-related reliability issues? **HosTalky** Overall, yes — reliable for day-to-day use. That said, we do have a handful of known sync edge cases we are actively working through, mostly around reconnect timing: a dropped connection during resync, or a cold start after being idle for a while, plus a couple of cross-device message delivery gaps. None of these are widespread, but they are real and tracked. We have also started implementing background updating on mobile to help with some of these cases. _**Editor’s note:** We’re working with the HosTalky team to see whether our recently released [Checkpoint Requests](https://docs.powersync.com/client-sdks/advanced/checkpoint-requests#sync-catch-up-checkpoint-requests) can help iron out these edge cases._ **PowerSync** What do you wish were different or better in PowerSync today? **HosTalky** Constructively: the main thing is that HIPAA eligibility — the BAA — sits on a higher plan tier than our usage otherwise requires. Surfacing the HIPAA-eligibility path and pricing earlier, and offering a route for pre-commercial teams that need the BAA but not the rest of the tier, would help companies at our stage. ## Closing thoughts The main thing that stuck with me from this conversation is how little AI-specific infrastructure HosTalky needed on the delivery side. Once “this note is still generating” became a field on the note rather than the state of an in-flight HTTP request, PowerSync did the rest: the state survives restarts, reaches every device the clinician is signed into, and needed no polling, retry, backoff or data dedupe code. --- # New Streamlined Pricing Model for PowerSync Cloud Enterprise > We are aligning the pricing metrics between the Pro, Team and Enterprise plans of PowerSync Cloud. Going forward, they will mostly use the same set of metrics. The result is pricing that is more transparent and easier to estimate up front. - Published: 2026-09-21 - Author: Conrad Hofmeyr - Category: Announcement - Source: https://powersync.com/blog/powersync-cloud-enterprise-pricing-model --- Since we launched PowerSync Cloud in 2023, we gathered some good learnings that allowed us to simplify our pricing models for customers. In late 2025, we announced [simplifications](https://powersync.com/blog/simplified-cloud-pricing-based-on-data-synced) to our Pro and Team plans, and now, we are announcing a simplified PowerSync Cloud Enterprise pricing model. ## What is changing The headline is that we are aligning the pricing metrics between the Pro, Team and Enterprise plan. Going forward, they will mostly use the same set of metrics: | Metric | Pro and Team Plans | Enterprise Plan | | :---- | ----- | ----- | | Data Synced | ✓ | ✓ | | Data Hosted | ✓ | ✓ | | PowerSync Service Instances | ✓ | ✓ | | Peak Concurrent Clients | ✓ | | | Average Concurrent Clients | | ✓ | As you can see, the only difference is the billing metric for concurrent clients: - Our Pro and Team plans will continue to use **Peak Concurrent Clients**. These plans are typically used for smaller deployments with relatively volatile usage patterns. Considering how our cloud system tends to provision infrastructure resources for these types of workloads, our costs are mainly correlated with the _peak_, and that is therefore the most appropriate metric for billing. - Our Enterprise plan will use **Average Concurrent Clients**. This plan is typically used for larger-scale deployments with more predictable workloads. Here, billing on the _average_ makes the most sense for us and our customers. ## How the new Enterprise pricing works To price your PowerSync Cloud Enterprise subscription, we will take the following approach: **1) Usage commitment:** You decide on a monthly usage commitment for each of the metrics: - Data Synced - Data Hosted - PowerSync Service Instances - Average Concurrent Clients **2) Discounted usage rates:** We provide you with a custom quote for your subscription, which includes: - A discounted rate for each of the billing metrics, relative to the rate on the Pro and Team plans. This means that Enterprise customers get economies of scale on their usage fees compared to the Pro and Team plans. For example, the reference rate on our Pro and Team plans for Data Hosted is $1 per GB per month. Let's say we quote you a 25% discount for Data Hosted. Your usage rate on the Enterprise plan would therefore be $0.75 per GB per month for that metric. - A base fee for the Enterprise plan. This is based on the scale of your deployment, and is calculated as a fixed percentage of the total usage-based fees per your usage commitment. **3) Monthly measurement:** If you exceed your committed usage on one of the metrics in a monthly period, overages will be billed based on an overage rate which is calculated as: your discounted usage rate plus 20%, or the reference rate for the Pro and Team plan – whichever is less. For example, let's say you committed to 15 GB of Data Hosted and your discounted usage rate is $0.75 per GB, and you have an overage of 2 GB in a month. That overage would be billed at $0.75 plus 20% = $0.90 per GB. ## Contract and billing periods By default, our Enterprise subscriptions involve an annual contract. The committed usage and base fee are therefore typically billed annually, and any usage overages are billed monthly in arrears if it meets a significance threshold (otherwise the overage billing is postponed to the following month). If you need to upgrade the baseline usage commitment in your subscription, we prepare a new quote and sign a new agreement that prorates fees for the remainder of your subscription term. ## Benefits to customers The main benefit to customers is that our new Enterprise pricing model is more transparent and easier to understand, compared to our previous custom pricing model which was admittedly somewhat of a 'black box'. We also expect it to be easier for customers to estimate their usage before committing to the Enterprise plan. We recommend that customers use the Free, Pro or Team plan to build a proof-of-concept and measure their metrics with small-scale testing, and then extrapolate those results to calculate the usage commitment required for the Enterprise subscription. We have added _Average Concurrent Clients_ as a metric we measure in PowerSync Cloud, and will soon expose it on the _Metrics_ section of the PowerSync Dashboard. ## Finer detail: dedicated storage clusters If you're familiar with our PowerSync Cloud Enterprise plan, you may know that we provide the option of provisioning a dedicated storage cluster for specific PowerSync Service instances where high scale is expected. In our new Enterprise pricing model, you have the option of selecting a dedicated storage cluster for one of your instances for every 200 GB of committed Data Hosted in your subscription. ## Migration of existing customers If you already have a PowerSync Cloud Enterprise agreement in place on a legacy pricing model, we will discuss migrating you to the new pricing model at the time of your agreement expiry/renewal. ## Questions? Feedback? If anything is unclear, if you need more information, or have any feedback, please [get in touch](https://powersync.com/contact). --- # Introducing Checkpoint Requests > Checkpoint Requests answer: has the local database caught up to the source database right now? This lets your app wait for the data it needs before proceeding. - Published: 2026-09-09 - Author: Steven Ontong - Category: Engineering - Source: https://powersync.com/blog/checkpoint-requests-client-synced-now --- ## Background PowerSync partially syncs your source database to a local in-app SQLite database. Partial sync is defined by a Sync Config: a YAML configuration containing Sync Stream definitions with SQL-like parameterized queries that describe the data to sync to each user. The PowerSync Service replicates relevant data from the source database, based on the Sync Config, into precomputed "buckets". Connecting clients receive bucket operations through a stream connection organized around **checkpoints**. A checkpoint usually maps to a set of complete source database transactions. A transaction's operations will never be split between checkpoints. The PowerSync Service and client SDK automatically synchronize the local SQLite with the source database. Developers can generally rely on local state being applied consistently at the next safe checkpoint. Reactive queries typically automatically update in-memory state when local SQLite changes are made. This is convenient for reacting to changes from both local (the current user) and externally sourced operations. For externally sourced operations, PowerSync guarantees that those remote changes would be applied at *some* point in the future, but historically there wasn't an easy way to definitively answer the question of "is the client fully synced *now*?" In the next section we explore this problem in a bit more detail. ## Wait for first sync PowerSync has historically exposed indicators for when the **first complete sync** has finished (to a freshly-initialized SQLite database). The client-side PowerSync database exposes a global `waitForFirstSync` method which resolves once the first complete sync has been applied locally. ```javascript // App initialization code await powerSync.waitForFirstSync() // App now knows the initial completely-synced data is available in SQLite // Queries should now reflect the user's data at this point in time ``` We also expose the first sync's completion on a per-stream basis: ```javascript const streamSubscription = await db .syncStream("todos", { list_id: "abc123" }) .subscribe(); await streamSubscription.waitForFirstSync(); await streamSubscription.waitForFirstSync(); ``` Those APIs are great for the specific use case of waiting on the *first complete sync*. However, what we've found from developer feedback is that apps often need to ensure that the local database has been synced *again* after the first complete sync. For example, consider this flow: - A user uses a PowerSync-enabled app. - App code which relies on `waitForFirstSync` gates certain logic, delaying until the sync completes. - The user closes the app. - The source database now contains changes which need to be synced to the user. - The user re-opens the app. - The first sync has already completed, so `waitForFirstSync` resolves. - The user has pending operations to sync, but the app has no clean way of waiting for those changes to be applied. - The app typically routes the user to specific views. For some workflows, the above is fine. Reactive queries typically update with the latest data once the incoming changes have been synced. For some other workflows, an application might **need** the latest data to be synced before allowing the user to proceed. A classic example of this could be push notifications. Let's say the application backend committed a change to the source database, which triggered a push notification to the device. The user tapped the notification related to the specific data, opening the application. App logic most likely will need the corresponding data to be queryable once handling the notification tap. Developers often find themselves needing an additional gating mechanism: waiting for the **next** checkpoint to be synced. ## Checkpoints ### (Legacy) Write Checkpoints An internal mechanism called Write Checkpoints is what the PowerSync client SDKs use to ensure data consistency. The general rule for consistency in PowerSync is: > An SDK client won't apply incoming sync operations until all its local mutations have been completed **and** the source database operations related to those mutations have been received back through the latest checkpoint. This [guarantees](https://docs.powersync.com/architecture/consistency#powersync-designed-for-causal+-consistency) causal consistency. The only exception to this is if [sync priority 0](https://docs.powersync.com/sync/advanced/prioritized-sync#special-case-priority-0) is used, but we'll ignore that for this explanation. The rule above is quite a mouthful, so we can break it down into an example. As a reminder, the PowerSync Service consumes a CDC replication stream from the source database and replicates its operations into precomputed [bucket storage](https://docs.powersync.com/architecture/powersync-service#bucket-storage). Consuming this stream requires some form of positional information. A typical mutation flows through PowerSync as follows: 1. The app makes the mutation via a SQLite query, e.g. `INSERT INTO my_table(...) VALUES (...)`. 2. The PowerSync client SDK queues this operation and other transaction operations in the upload queue. 3. The PowerSync client SDK calls the `BackendConnector` `uploadData` method at appropriate times, allowing the application code to fetch `CrudTransaction`s or `CrudBatch`es. - The connector passes the mutations to the application backend, where the application backend may modify, persist or deny those mutations from entering the source database. - The `.complete()` method is called on the `CrudTransaction` or `CrudBatch`. This removes the entries from the upload queue. 4. Once the upload queue is empty, the PowerSync client SDK requests a Write Checkpoint from the PowerSync Service. - The PowerSync Service associates the current source database replication position with the user and client that requested the Write Checkpoint. - The PowerSync Service returns a marker to the client, which is a target the SDK uses to wait for the corresponding checkpoint to be received from the PowerSync Service. The SDK won't apply any incoming operations to the local SQLite tables until it has fully received the corresponding checkpoint. You might ask yourself, why are we talking about uploads when we want to know when the PowerSync client SDK has synced a checkpoint? The reason for all that context is that historic Write Checkpoints have been answering that question all along. It's just been an internal facet of PowerSync. Write Checkpoints have allowed the client SDK to wait for a specific source database state to be present locally; we'd need something similar if we'd like to wait for subsequent checkpoint syncs. ### Checkpoint Requests We recently released the Checkpoint Request API across all SDKs, requiring PowerSync Service version 1.24.0 or later. It allows developers to answer the question, "Has the application synced all relevant changes in the source database up to now?" Checkpoint Requests use the PowerSync Service to make an association with the source database's current replication position. This association is tracked on the client and service side. The PowerSync Service reports to the SDK once the data related to the association has been sent to the client. The client can then notify application code once the Checkpoint Request has been applied. After it's applied, the local SQLite tables will contain all relevant state corresponding to the source database at the time of the association. Checkpoint Requests require a connected or connecting PowerSync SDK client. A request cannot be created while the client is fully disconnected. While the client is connecting, request creation waits until the PowerSync Service is reachable. If the connection drops while waiting for a created request, the app can reconnect and wait on the same request again. ```javascript // Make sure to connect using the new `checkpointMode` await db.connect(connector, { checkpointMode: 'requests' }); // Perform some operation // ... // Create a checkpoint request const checkpoint = await db.requestCheckpoint(); // Wait for the checkpoint to sync, with a 30-second timeout await checkpoint.waitForSync({ signal: AbortSignal.timeout(30_000) }); // Now the data is available in the local SQLite database ``` > **Note** > > **Note:** Checkpoint Requests are currently in alpha. Write Checkpoints still exist, but in the future we will completely replace legacy Write Checkpoints with Checkpoint Requests. ## Use cases ### Waiting for the next sync For the notification example mentioned, an app can now create a Checkpoint Request and wait for it to be synced when handling the notification app-open event: ```javascript async function handleNotification() { // ... logic const checkpoint = await db.requestCheckpoint(); await checkpoint.waitForSync({ signal: AbortSignal.timeout(30_000) }); // The app can now query for data related to the notification event // and navigate to specific views if necessary. } ``` ### How we use Checkpoint Requests in the Dashboard We're using Checkpoint Requests in the PowerSync Dashboard itself to handle a subtle timing issue: a management API call could succeed before the resulting change reached the browser's local database. After creating an instance, for example, the dashboard could try to open its page before its data was available locally. Requesting a checkpoint after the API call lets us wait for local sync to catch up before navigating. ### Waiting for writes The PowerSync client SDK uses the same mechanism for Checkpoint Requests as it does for the historic Write Checkpoint gates mentioned above. This means that the existing consistency gates and Checkpoint Requests are inherently linked. For Write Checkpoints: > An SDK client won't apply incoming sync operations until all its local mutations have been completed **and** the source database operations related to those mutations have been received back through the latest checkpoint. This ties Checkpoint Requests and local mutations. A user can now write code like this: ```javascript async function someMutation() { // Perform some local writes await db.execute( `INSERT INTO ${TODOS_TABLE} (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)`, [ connector.currentUserID, todo.description, listId, todo.isComplete ? 1 : 0 ] ); // This queues the local write for asynchronous upload. // Create a checkpoint request and wait for it to sync. const checkpoint = await db.requestCheckpoint(); await checkpoint.waitForSync({ signal: AbortSignal.timeout(30_000) }); // The PowerSync client SDK and Service associate the request with the // current database replication position and wait for that data to be // synced and applied locally. // // The consistency barriers also ensure that the wait completes only // after the local write has been uploaded and the corresponding // checkpoint has been synced and applied locally. // // The application backend has now processed the local write above, // and the result of that processing is available locally. // We can query the database for any additional changes the backend // made when creating the TODO record. } ``` The code above is possible due to our gating logic for applying incoming changes while local mutations are in the upload queue. Essentially, we perform a local mutation which queues the item for upload. We also create a Checkpoint Request after the mutation. If that explicit request is created while the upload is pending, the SDK creates a newer internal request after the upload queue empties. We'll only get a resolve once the effective checkpoint has been synced. **Note:** The exact request ordering is not deterministic. The upload may complete and create its internal Checkpoint Request before the explicit `db.requestCheckpoint()` call. In either order, the checkpoint gate remains valid and the wait only completes after the relevant upload state and requested source database state have been applied locally. This assumes that `uploadData()` returns only after the application backend has committed the uploaded changes to the source database. Application backends that process uploads asynchronously should use Custom Checkpoint Request handlers by implementing `CustomCheckpointRequestConnector`, so the checkpoint is created only after the queued work has been processed. See additional details in our [documentation](https://docs.powersync.com/client-sdks/advanced/checkpoint-requests#asynchronous-upload-backends). This effectively provides a means for waiting for the upload queue to drain and the next checkpoint to be synced. ### Intermittent sync Now that we can wait for the next sync, we can temporarily connect a client to PowerSync, wait until the requested checkpoint has been fully applied, then disconnect. This could be useful in cases where apps don't need to be permanently connected to the PowerSync Service. ```javascript async function sync() { await db.connect(connector, { checkpointMode: 'requests' }); const checkpoint = await db.requestCheckpoint(); await checkpoint.waitForSync({ signal: AbortSignal.timeout(30000) }); // No need to stay connected after we've got the latest data await db.disconnect(); } ``` ## Conclusion PowerSync already keeps the local SQLite database continuously and reactively in sync. Checkpoint Requests add an explicit synchronization boundary: an application can ask the PowerSync Service to capture the source database's current replication position, then wait until all relevant data through that position has been applied locally. `waitForFirstSync` answers, "Has this database completed its initial sync?" A Checkpoint Request answers, "Has this database caught up to the relevant source database state *now*?" That distinction makes flows such as notification handling, explicit refreshes, write-dependent actions and intermittent sync easier to coordinate without polling or relying on arbitrary delays. In short, Checkpoint Requests turn an eventual catch-up into an observable application event. When an app needs to know that relevant source data is locally available before it proceeds, it can now wait for that point directly. --- # Offline-First at National Scale > Four public-sector deployments — a national school meals program, a government retail network, a telecoms field workforce and a social housing repairs operation — and the shared constraints behind them: no network, in-country data, and scale proven before development starts. - Published: 2026-08-17 - Author: Conrad Hofmeyr - Category: Showcase - Source: https://powersync.com/blog/offline-first-at-national-scale --- A field test of PowerSync for a government project last year covered five schools in a mountainous region in Asia. One school had around 20 Mbps of connectivity. Two ran between zero and one megabit. Two had no coverage at all, and in those the procedure was for a staff member to travel to a location with signal roughly once every ten days and sync from there. Guaranteed local retention for extended offline durations (weeks) was baked into the requirements, so that a missed trip would not cost anyone their records. Testing ran on low-end Android handsets and on recent iPhones, and the application performed well across all five sites. Requirements like that are ordinary in the public sector. Connectivity is rarely negotiable, because a government application has to cover everyone within a jurisdiction, including the parts of it where the network has not been built. In-country data residency is non-negotiable. And the user count is derived from the size of the population being served, which means it is known before development starts and scalability has to be demonstrated early on during evaluation. ## PowerSync Public-Sector Deployment Examples Here are a few anonymized representative examples of PowerSync public-sector deployments: ### A National School Program, Asia A national education authority is implementing a PowerSync-enabled software application that records daily attendance and meal distribution at schools. The application aggregates those figures up through a regional hierarchy all the way to the national level. It is built in Flutter and runs on Android and iOS devices already owned by school staff. The upcoming national rollout will involve hundreds of millions of records captured per day across millions of students and thousands of schools. The team initially intended for senior administrators to sync a single summary table of roughly two million rows, with a target initial sync under two minutes. That target was not achievable at that row count with PowerSync. The workable approach was restructuring: separate aggregated tables per administrative level, so an administrator syncs a few dozen rows per day and a regional administrator syncs under a thousand. The data volume on the wire dropped by three orders of magnitude. The scale of this project breaks assumptions outside the sync layer as well. The team had been using a commercial hosted identity provider and dropped it once the per-user pricing was extrapolated to a national rollout, replacing it with their own JWT signing. PowerSync is not an identity provider. It verifies JWTs from whatever mints them, which means the identity system each organization already runs. Accuracy is what a program like this gets judged on, and that is the citizen-facing outcome here. A meal served in a school without connectivity is recorded at the point of service, and it reaches the government as captured data instead of being reconstructed from paper or memory some weeks later. Parents will eventually get a view of the same records. A parent account will only sync its own children's data, enough to show whether a child was marked present and received a meal on a given day. Parent logins are scheduled for a release after the school-level features that make up the first rollout, and they change the shape of the load considerably. Parent sessions are short, a few minutes each, and they cluster almost entirely between the start of the school day and mid-morning, so the deployment has to absorb millions of brief connections inside a few hours. ### Nationwide Government Retail Locations, Asia A technology partner is building an offline-first point-of-sale system for a government program that involves tens of thousands of retail locations across the country. The software is built in Flutter and runs on low-cost Android point-of-sale terminals. Connectivity at these sites is intermittent, often for hours at a time, and a point-of-sale system that depends on a live server connection stops operating during those windows. With PowerSync, transactions complete on the terminal and reconcile when the link returns, so retail locations keep trading and citizens are served. The deployment is self-hosted on managed Kubernetes, with Postgres as both the source database and the bucket storage. ### Field Technicians, Asia-Pacific A government-owned telecommunications infrastructure operator is building a native iOS application for its field workforce, numbering in the thousands of technicians. The previous application was a PWA. It handled offline conditions poorly, and iOS updates periodically broke the runtime beneath it. The replacement is written in Swift and SwiftUI, with PowerSync self-hosted inside the operator's own cloud environment to satisfy in-country data residency requirements. Work orders, calendars, messages and notifications sync to the device. Writes are uploaded through the operator's existing API gateway and backend services, which allows the existing authentication and security model to remain unchanged. For households waiting on a connection or a repair, the effect is fewer repeat visits. The technician arrives with the complete job record regardless of coverage at the site and can close the work on the first appointment. ### Social Housing Maintenance, Europe The same pattern turns up again in a housing and care provider that maintains tens of thousands of homes on behalf of local and central government. Its repairs and maintenance application runs on thousands of devices and several of its contracts carry data sovereignty terms that require hosting within the country. Repairs are recorded at the property, including inside buildings with no reception, which keeps tenant repair histories accurate and complete. ## Security Review and Continuity Risk In a public-sector deal, the information security review usually asks for things like a SOC 2 report, third-party penetration test results, static analysis and dependency scan output, and a vendor willing to work through a long security questionnaire. A harder question that often comes up is what happens if the vendor discontinues the product. That is a fair question to put to us, and two of the four customers above are here because it happened to them: both were running MongoDB Atlas Device Sync when it was deprecated. PowerSync inherently provides continuity guarantees. The PowerSync client SDKs are Apache 2.0 licensed. The PowerSync Service is available as Open Edition under the Functional Source License, which converts to Apache 2.0 over time, so a self-hosted deployment does not depend on our continued existence to keep running. ## Resilience on the Device PowerSync is designed to support devices offline for extended periods — weeks at a time or even longer. It is also built to assume unreliable client-side storage. PowerSync maintains checksums per bucket of data on both the client and the server, and validates them as part of every checkpoint. A mismatch should never happen during normal operation, but local storage on inexpensive Android hardware does fail, and processes get killed mid-write. When a bucket checksum does not match, PowerSync re-downloads that bucket automatically. ## Running the Service A single PowerSync instance supports on the order of 50,000 to 100,000 concurrently connected clients depending on the shape and volume of the synced data, and deployments beyond that use a sharded architecture. All four example deployments above run PowerSync self-hosted, in each case for residency or sovereignty reasons. It is the same service image with the same sync semantics as PowerSync Cloud, configured through YAML files or admin APIs rather than a dashboard, and it exposes Prometheus metrics and a diagnostics endpoint for monitoring in Grafana or an equivalent. Teams that already run their own infrastructure tend to want their sync layer visible in the same dashboards as everything else. ## What These Deployments Have in Common A school meals program, a government retail network, a telecoms field workforce and a social housing repairs operation have very little to do with one another, but their requirements are similar nevertheless. Each one needed the application to keep working with no network, the data to stay inside the country, and scalability and reliability to be demonstrated early on. If you are scoping something with these characteristics, our team is available to work through sizing and architecture before you commit to a design. --- # Migrate from Electric Cloud to PowerSync Cloud > A guide for teams moving from Electric Cloud to a managed, non-Electric cloud provider: PowerSync Cloud. It covers the key concepts and migration steps. - Published: 2026-08-12 - Author: Kobie Botha - Category: Tutorial - Source: https://powersync.com/blog/migrate-from-electric-cloud-to-powersync-cloud --- Electric [announced](https://electric.ax/blog/2026/08/11/electric-joining-databricks) that it has joined Databricks and Electric Cloud has shut down. The announcement guidance includes self-hosting Electric with another cloud provider. This guide is for teams that instead want to move to a managed cloud provider that is not Electric: PowerSync Cloud. This implementation guide is for teams already using Electric's current Postgres Sync service that want an alternative managed cloud service, rather than self-hosting Electric or using Neon. It assumes Postgres remains the system of record and covers the high-level concepts and steps needed to migrate. ## Concepts PowerSync Sync Streams are analogous to Electric Shapes, not Electric Streams. Electric Streams is a separate product and is out of scope for this guide. While Electric Shapes and PowerSync Sync Streams both control partial sync, there are significant differences meaning that Shape to Sync Stream migration will most likely not be 1-for-1, and re-designing partial sync will most likely be required. A key architectural difference between Electric Shapes and Sync Streams is that Shapes are created on demand from the client, per request, while Sync Streams are declared as named queries ahead of time that clients then dynamically subscribe to when needed, each subscription passing its own parameters. This architecture drives how authorization, offline behavior, and writes work. The table below summarizes the specific differences between Electric Shapes and Sync Streams across these and other key dimensions: | Dimension | Electric (Shapes) | PowerSync (Sync Streams) | | --- | --- | --- | | Definition | Client-side, per request. Using `table` and optional `where`, `columns`, and `queryable_columns` parameters. Each parameter set identifies a distinct Shape. | Server-side, names SQL queries in a deployed YAML config. Clients subscribe by name and supply parameters; `auto_subscribe` starts a stream automatically. | | Relational scope | Single table only. Use subqueries to filter, or multiple Shapes to sync related data. | `JOIN` supported. One stream can carry multiple queries. | | Filter language | Supported PostgreSQL `WHERE` expressions, positional parameters, arrays, and subqueries. No non-deterministic functions. [[ref](https://electric.ax/docs/sync/guides/shapes#supported-operators)] | SQL subset with inner joins, subqueries, CTEs, and transformations; no `GROUP BY`, `ORDER BY`, `LIMIT`, or `UNION`. No non-deterministic functions. [[ref](https://docs.powersync.com/sync/supported-sql)] | | Authorization model | "Production apps should request shapes through your backend API for authorization and security". [[ref](https://electric.ax/docs/sync/guides/shapes#defining-shapes)] | Sync Stream queries define download access using signed JWT claims. Treat client-provided subscription parameters as untrusted. | | Client store | In-memory rows by default. Persistence and offline behavior require a separate client store or integration, such as PGlite or TanStack DB collections. | SQLite with a declared `AppSchema`. The Web SDK uses persistent IndexedDB by default; additional VFSes are available, and an in-memory VFS is optional. | | Local queries | `shape.rows`, `useShape`, TanStack DB live queries. | SQL against SQLite (`useQuery`, `watch()`). Also supports the TanStack DB interface and SQLite ORMs like Drizzle. | | Writes | Electric provides read-path sync only. The app defines its write path. | Local `INSERT`, `UPDATE` and `DELETE` operations enter a FIFO upload queue. Developer-defined `uploadData()` applies mutations via the developers' existing backend which authorizes and applies them. | | Primary keys and types | Must always include Postgres primary key columns. | Each synced table requires a single unique text `id` column. Columns can be aliased, concatenated or casted [[ref](https://docs.powersync.com/sync/advanced/client-id#client-id)]. The PowerSync TanStack DB integration provides automatic support for rich types. [[ref](https://tanstack.com/db/latest/docs/collections/powersync-collection)] | | Performance consideration | Shape filter design affects throughput. [[ref](https://electric.ax/docs/sync/guides/shapes#throughput)] | Bucket cardinality affects sync performance. [[ref](https://docs.powersync.com/sync/rules/organize-data-into-buckets#limit-on-number-of-buckets-per-client)] | ## Migration Note that both Electric Sync and PowerSync codebases are public, so agents should generally not struggle with implementing migrations. Also see the PowerSync [agent resources](https://docs.powersync.com/tools/ai-tools). If you need assistance, join the [PowerSync Discord](https://discord.gg/powersync). ### Overview of Migration Steps 1. [Configure Postgres](#configure-postgres) 2. [Connect PowerSync](#connect-powersync-to-postgres) 3. [Define and test Sync Streams](#define-and-test-sync-streams) 4. [Set up Authentication](#set-up-authentication) 5. [Migrate the frontend code](#migrate-the-frontend-code) ### Configure Postgres 1. Create a Postgres role and publication as per [documentation](https://docs.powersync.com/configuration/source-db/setup#supabase). 1. Note: it is not possible to reuse the Electric publication, since PowerSync requires a publication named `powersync`. 2. Note that `BYPASSRLS` is commonly used for the PowerSync role. This is because Sync Stream definitions enforce authorization. ### Connect PowerSync to Postgres 1. Connect your PowerSync instance to your Postgres environment 1. Dashboard [instructions](https://docs.powersync.com/configuration/source-db/connection#supabase) 2. CLI [instructions](https://docs.powersync.com/tools/cli#cloud-workflows) 2. Since the PowerSync Service connects directly to Postgres, various network-level security mechanisms are [supported](https://docs.powersync.com/configuration/source-db/security-and-ip-filtering#security-and-ip-filtering). ### Define and Test Sync Streams In this step you will write your Sync Streams YAML. This will: - Define how subsets of your Postgres data are synced to SQLite (on the client) - Move download authorization into Sync Stream queries. You keep write authorization in your backend. PowerSync uses signed JWT claims for access checks. Sync Streams accept client parameters, but these should not be relied on for authorization checks. - Refer to the Sync Streams [documentation](https://docs.powersync.com/sync/streams/overview) - Note that if the [supported Sync Streams SQL](https://docs.powersync.com/sync/supported-sql) doesn't support the specific query you are trying to write, sync the required streams and then run `ORDER BY`, `LIMIT`, aggregates, joins, etc. in local SQLite. #### Example A Shape that syncs a user's projects as follows: ```typescript const { data } = useShape({ url: `http://localhost:3000/v1/shape`, params: { table: `projects`, where: `owner_id = ${currentSession().userId}`, }, }) ``` Can be defined in Sync Streams as this `my_projects` stream: ```yaml streams: my_projects: query: SELECT * FROM projects WHERE owner_id = auth.user_id() ``` You can then define a separate Sync Stream to sync each project's tasks (`project_tasks` stream), where the client provides the `project_id`: ```yaml streams: my_projects: ... project_tasks: query: | SELECT * FROM tasks WHERE project_id = subscription.parameter('project_id') AND project_id IN (SELECT id FROM projects WHERE owner_id = auth.user_id()) ``` #### Run a Sync Test from the PowerSync Dashboard Once you've defined your Sync Streams, you can run a Sync Test in the PowerSync Dashboard to validate that data is syncing to the client as expected. ### Set Up Authentication Electric relies on a backend to validate requests to Shapes. With PowerSync that logic is contained in your Sync Stream queries, and clients are then able to connect to the PowerSync Service with a JWT minted from your backend, instead of using the backend as a step in the middle. `auth.user_id` illustrates this in the Sync Streams example from above: ```yaml query: SELECT * FROM projects WHERE owner_id = auth.user_id() ``` Our [authentication documentation](https://docs.powersync.com/configuration/auth/overview) covers how to set this up. ### Migrate the Frontend Code The PowerSync client owns a local SQLite database that intelligently merges rows from all active Sync Streams. It exposes local SQL and live query (watch) APIs. It optionally records local writes into a FIFO upload queue. Note that migrations from PGlite to SQLite are possible but out of scope for this guide. PowerSync provides SDKs for many platforms: Swift, Kotlin, Dart, .NET, Rust and JS/TS (Web, React Native, Node, Capacitor and Tauri). This section only covers Web JS/TS. 1. [Install](https://docs.powersync.com/client-sdks/reference/javascript-web#installation) the PowerSync Web SDK with `pnpm install @powersync/web` 2. Use the PowerSync Dashboard or CLI to generate the client-side SQLite schema ([docs](https://docs.powersync.com/client-sdks/reference/javascript-web#1-define-the-client-side-schema)). Note that an `id` column is added automatically. It will look similar to this: ```typescript import { column, Schema, Table } from '@powersync/web' const projects = new Table({ name: column.text, owner_id: column.text, }) const tasks = new Table({ project_id: column.text, title: column.text, status: column.text, }, { indexes: { by_project: ['project_id'] } }) export const AppSchema = new Schema({ projects, tasks }) ``` 3. [Instantiate](https://docs.powersync.com/client-sdks/reference/javascript-web#2-instantiate-the-powersync-database) the PowerSyncDatabase. Note that you must only create one `PowerSyncDatabase` instance for each database file ```typescript import { PowerSyncDatabase } from '@powersync/web' export const db = new PowerSyncDatabase({ schema: AppSchema, database: { dbFilename: 'powersync-v1.db' }, }) ``` 4. The Web SDK uses a persistent IndexedDB VFS by default. [Select another VFS](https://docs.powersync.com/client-sdks/reference/javascript-web#sqlite-virtual-file-systems) if you have specific browser, performance, or multi-tab requirements. 5. [Integrate](https://docs.powersync.com/client-sdks/reference/javascript-web#3-integrate-with-your-backend) with your backend. This requires implementing two methods for `PowerSyncBackendConnector`: `fetchCredentials` and `uploadData` 1. `fetchCredentials()` returns the JWT used to authenticate against PowerSync with and download synced data, as well as the PowerSync Cloud endpoint 2. `uploadData()` defines how local mutations are sent to your backend API - you should be able to re-use the existing backend you have in place today 6. Replace client-side queries. This is where the bulk of the work will take place, but agents should be pretty good at it. Below are some more tips to get you going: 1. Replace shape [handle/offset usage](https://electric.ax/docs/sync/guides/shapes#subscribing-to-shapes) with [`waitForFirstSync()`](https://docs.powersync.com/client-sdks/usage-examples#wait-for-the-initial-sync-to-complete) and Sync Stream [status checks](https://docs.powersync.com/sync/streams/client-usage#checking-sync-status). 2. Tie on-demand Sync Stream subscriptions to component or route lifetime. The PowerSync React Hooks can [automatically subscribe/unsubscribe](https://docs.powersync.com/sync/streams/client-usage#framework-integrations). 3. Note that when subscribing to a Sync Stream on the client, the TTL can be overridden. The default is 24 hours: a shorter TTL reduces disk usage, a longer TTL improves page reload performance. 4. When logging the user out or switching accounts, only use `disconnectAndClear()` once the upload queue has been emptied, otherwise local mutations will get discarded. 7. Implement writes 1. Electric is read-path only, so this guide doesn't cover migrating a write path. However, it's highly recommended to use the PowerSync upload queue to ensure consistency instead of sending mutations directly to your backend. You might notice data flicker on the client if you write directly to your backend APIs / bypass the PowerSync upload queue. 2. Follow [this guide](https://docs.powersync.com/configuration/app-backend/client-side-integration) for integrating the PowerSync upload queue with your backend APIs --- # PowerSync Changelog: June & July 2026 > We released v2.0 of our JavaScript SDKs, added support for Azure DocumentDB and Convex as source databases, shipped an official Terraform provider, and merged some of the most substantial community contributions we've had yet. - Published: 2026-08-11 - Author: Kobie Botha - Category: Product Update - Source: https://powersync.com/blog/powersync-changelog-june-july-2026 --- In the last edition I wrote about overhauling and shipping v2.0 of our Dart SDK. It's important to us that PowerSync is well-maintained, so this time we extended that effort to our JavaScript SDKs. We tackled a long laundry list of DX issues and released v2.0 of our web, React Native, Capacitor and Node.js SDKs. More on that in the feature story below. Beyond that, we added support for new source databases (Azure DocumentDB and Convex), released an official Terraform provider, and shipped some of the most substantial community contributions we've had yet. Community members adding non-trivial features is becoming a trend, and we love it. ## Product updates shipped **PowerSync Service:** * **Azure DocumentDB as a source database**: DocumentDB (formerly Azure Cosmos DB for MongoDB vCore) now works through our existing MongoDB connector: link your connection string and PowerSync detects it automatically. [Release notes](https://releases.powersync.com/announcements/announcing-azure-documentdb-support-experimental). * **Convex as a source database**: Building with Convex and need offline support? You can now connect your Convex database to PowerSync. Writes still go through your existing Convex mutations, and each client only syncs the data it needs. It's an experimental release, and your feedback shapes where we go from here. [Release notes](https://releases.powersync.com/announcements/announcing-convex-backend-support-experimental) and [design notes](https://powersync.com/blog/convex-powersync-design-notes). * **Sync Streams hardening**: The compiler now warns about queries that can silently sync rows under the wrong name (unquoted JOIN aliases, `SELECT *` after an aliased column). We also fixed several cases where our evaluator diverged from SQLite semantics (thanks @sravan27!). [Changelog](https://releases.powersync.com/announcements/powersync-service). **Client SDKs:** * **JavaScript SDKs v2.0**: Focused on a more stable and intuitive public API, and simpler setup. See the feature story below or review the [release notes](https://releases.powersync.com/announcements/v2-0-of-powersync-javascript-sdks) for a complete summary and how to handle breaking changes. * **Multi-process SQLite access (Swift v1.15.0)**: App Extensions and App Groups are now supported: the database can live in an App Group container, with the SDK coordinating access between your app and its widgets or extensions. This is an early release with known footguns. We have an intuition that better DX is possible here, but we need your feedback to shape this. [Release notes](https://releases.powersync.com/announcements/powersync-swift-sdk) and [Docs](https://docs.powersync.com/client-sdks/reference/swift#app-groups-and-app-extensions). * **In-memory databases for the web (v1.39.0)**: `WASQLiteVFS.InMemoryVfs` keeps the whole database in memory with no persistence, which is useful for local development and online apps. [Docs](https://docs.powersync.com/client-sdks/reference/javascript-web#3-in-memory-vfs). * **Bring your own HTTP client (Dart/Flutter v2.3.0)**: `SyncOptions.httpClient` lets you add headers, trust self-signed certs, or swap in a faster client like `cronet_http`. SQLCipher is also back as a native encryption option. [Docs](https://docs.powersync.com/client-sdks/reference/flutter#custom-http-clients-and-headers). * **Room 3.0 (Kotlin v1.14.1)**: Our Room integration for Kotlin now uses Room 3.0. This is a breaking change: update your project to Room 3.0 when upgrading to v1.14.1. [Docs](https://docs.powersync.com/client-sdks/orms/kotlin/room). **Tools:** * **Official Terraform provider**: Manage PowerSync Cloud projects and instances as Infrastructure-as-Code. [Release notes](https://releases.powersync.com/announcements/an-official-terraform-provider-for-powersync-1). * **Custom roles and permissions (Dashboard)**: Team and Enterprise customers can now create and assign custom Dashboard roles themselves, with project and instance scoping and fine-grained permissions. Previously this had to be configured by our team. [Docs](https://docs.powersync.com/tools/powersync-dashboard#roles-and-permissions). * **More ways to search instance logs (Dashboard)**: Building on May's [searchable logs release](https://releases.powersync.com/announcements/searchable-instance-logs-in-the-powersync-dashboard), we added additional quality-of-life improvements such as excluding terms with a `-` prefix and clicking a timestamp to set the range start or end. [Docs](https://docs.powersync.com/maintenance-ops/monitoring-and-alerting#instance-logs). * **Smarter CLI commands (v0.10.0)**: Commands that take `--instance-id` now resolve the project and org from it automatically (previously you had to add these as flags yourself). [Docs](https://docs.powersync.com/tools/cli). **New demos & guides:** * [It's easier than ever to add your own source DB connector](https://github.com/powersync-ja/powersync-service/tree/main/docs/replication) to PowerSync. Steven documented how replication in the PowerSync Service works — knowledge that until now mainly lived in people's heads. * [Supported platforms reference](https://docs.powersync.com/resources/supported-platforms) — we fleshed out this page to include more detail, like minimum platform versions per SDK, so you can quickly check whether PowerSync runs on your target platform. * [.NET attachments demo](https://github.com/powersync-ja/powersync-dotnet/tree/main/demos/MAUITodo) — the MAUI To-Do demo now also shows offline-first file syncing with the attachment APIs from v0.1.2. * [Load custom SQLite extensions](https://docs.powersync.com/client-sdks/advanced/sqlite-extensions) — how to load custom SQLite extensions into the client-side database. ## Community feed * Conrad wrote about how we built our internal company brain: data from 14+ systems aggregated into Postgres, synced to local SQLite with PowerSync, and queried by agents from Slack, a CLI, or fully offline with local models. [Read the post](https://powersync.com/blog/building-our-company-brain-agents-sqlite-offline-capable). * Manrich shared the dogfooding story of rebuilding the PowerSync Dashboard on our own sync engine. [Read the post](https://powersync.com/blog/rebuilding-powersync-dashboard). * Dean, Christiaan, Steven, and Simon manned our booth at Local-First Conf in Berlin. Thanks to everyone who came to chat! [Talks are on YouTube](https://www.youtube.com/playlist?list=PLXxEKA_dxoH0). * [@sravan27](https://github.com/sravan27) fixed several cases where Sync Streams evaluation diverged from SQLite semantics, which could silently sync the wrong rows ([json_each](https://github.com/powersync-ja/powersync-service/pull/647), [division by zero](https://github.com/powersync-ja/powersync-service/pull/646), [signed casts](https://github.com/powersync-ja/powersync-service/pull/645), [alias warnings](https://github.com/powersync-ja/powersync-service/pull/662)). Disclosure: most of this came out of a paid hardening sprint we ran with Sravan. Thank you for the solid work. * [@asiergmorato](https://github.com/asiergmorato) added multi-process database support to the Swift SDK, which became the App Groups feature ([PR #147](https://github.com/powersync-ja/powersync-swift/pull/147)). He needed it for his own app, [Fitwoody](https://fitwoody.camp/). Also thank you for your [Swift Data integration](https://github.com/powersync-community/swift-data), which lets Swift Data models persist and sync through PowerSync. * [@mandrade2](https://github.com/mandrade2) tried out [our Tauri integration tests](https://powersync.com/blog/tauri-integration-tests), hit an error running them in Vitest's iframe, fixed it in [PR #991](https://github.com/powersync-ja/powersync-js/pull/991), and we've since updated the post with his fix. We love this kind of feedback loop, thank you! * Thank you also to [@henriquekraemer](https://github.com/powersync-ja/powersync-service/pull/734), [@marioortizmanero](https://github.com/powersync-ja/powersync-kotlin/pull/357), [@rumitvn](https://github.com/powersync-ja/powersync.dart/pull/432), [@delagen](https://github.com/powersync-ja/powersync-js/pull/1016), [@sincraianul](https://github.com/powersync-ja/powersync-js/pull/1051), [@daniel-vacic](https://github.com/powersync-ja/powersync-native/pull/23), [@Ishant5436](https://github.com/powersync-ja/powersync-cli/pull/56), [@shenlong-tanwen](https://github.com/powersync-ja/sqlite_async.dart/pull/145), and [@VIVAAN-DHAWAN](https://github.com/powersync-ja/powersync-js/pull/975) for your contributions — we're seeing more and more substantial features and fixes from the community and are loving it. * Yahya ([@whygee-dev](https://github.com/whygee-dev)) built [powersync-mdbx](https://github.com/whygee-dev/powersync-mdbx), a research reimplementation of the PowerSync replication protocol in Rust + MDBX, with promising benchmarks. It's not a drop-in replacement today, but as Ralf put it: "It's a very good observation that the PowerSync model fits quite well into a KV store, and does not need a full database like MongoDB or Postgres." The thread is in [`#engineering-discussions`](https://discord.com/channels/1138230179878154300/1504213613009834145/1527822415516663828). * New proposal: [SQLite as sync bucket storage](https://github.com/powersync-ja/powersync-service/pull/674). Steven is experimenting with SQLite (and MySQL) as bucket storage options, which would let self-hosted setups run the PowerSync Service without a separate MongoDB or Postgres storage database. * New proposal: [Connection count & error metrics](https://github.com/orgs/powersync-ja/discussions/661). This would add a counter to our existing sync API metrics for the total number of completed sync connections, labelled by outcome, error code, close reason, and transport. You'd then be able to answer questions like "were there recent sync error spikes?" with a simple Prometheus query. ## Feature story: JavaScript SDKs v2 Over time, our JavaScript SDKs accumulated a lot of DX issues, and more recently AX issues too. So we took a step back and asked: what would make these SDKs better to work with and contribute to, for both humans and agents? We kept looking at the public API itself. `@powersync/common` exposed the SDK's internals alongside its public classes, so routine changes on our side could break existing builds or produce confusing type errors. Taking `AbstractPowerSyncDatabase` as an example, which was both a class and an effective public interface: even adding a method to it counted as a breaking change under TypeScript's structural rules. The day-to-day ergonomics had rough edges too. The options you passed to `PowerSyncDatabase` were spread across a nested chain of interfaces, so there was no single place to see what was available. The `database` field accepted a `DBAdapter`, an `SQLOpenFactory`, or open options all at once, so autocomplete showed every `DBAdapter` method instead of the options you wanted. The `js-logger` dependency caused problems when bundling. And WebSocket support was bundled whether you used it or not, which was not cheap: it relied on RSocket, whose packages are around 500 kB and need a custom build step to work outside Node.js. None of these were dealbreakers on their own, but together they made the SDKs harder to work with than they should be. And not just for people: coding agents tripped over the same confusing types and scattered options. So we fixed the foundation. Benefits of the JavaScript SDKs v2.0 release include: * A cleaner, more stable API, with only public interfaces exposed and implementation details in a separate package * Simpler setup, with one clearly-typed set of options for the database, and one for `connect()` * Logging as a small `PowerSyncLogger` interface rather than a separate dependency * Smaller bundles, with WebSockets loaded lazily and the two web workers merged into one * OP-SQLite built into `@powersync/react-native` as the default driver, replacing React Native Quick SQLite There are breaking changes, but for most apps the upgrade is a few mechanical edits to how you construct the database, connect, and set up logging. The [release notes](https://releases.powersync.com/announcements/v2-0-of-powersync-javascript-sdks) walk through every one. A lot of what went into v2.0 came straight out of your feedback and sharing how you're using these SDKs. Please keep it coming. That's it for this issue. --- # Building Our Company Brain: Agents + SQLite, Offline-Capable > How we built an internal "company brain" by aggregating data from 14+ systems into Postgres, syncing it to a local SQLite database with PowerSync, and giving agents a skill to query it — usable from Slack, a CLI, and even offline with local models. - Published: 2026-07-24 - Author: Conrad Hofmeyr - Category: Showcase - Source: https://powersync.com/blog/building-our-company-brain-agents-sqlite-offline-capable --- By now you've probably heard of the idea of a "second brain" or "company brain". Here's [YCombinator's Summer 2026 Request for Startups](https://www.youtube.com/shorts/IaWIazkWWog): > Every company has critical know-how scattered everywhere. Some of it lives in people's heads. Some of it is buried in old email accounts, Slack threads, support tickets, and databases. The company works because humans vaguely remember where that knowledge is and how to apply it. But AI agents can't operate like that. If we want every company to run on AI automation, we need a new primitive: a company brain. [...] A system that pulls knowledge out of all these fragmented sources, structures it, keeps it current, and turns it into an executable skills file for AI. In April 2026, Andrej Karpathy's tweet about research-focused [LLM Knowledge Bases](https://x.com/karpathy/status/2039805659525644595?lang=en) inspired a lot of people to simply have their agent use a folder of Markdown files to create and maintain a kind of 'second brain' with a collection of useful knowledge. YCombinator CEO Garry Tan created his [GBrain](https://github.com/garrytan/gbrain) project directly inspired by Karpathy's idea, for example. While a "folder of Markdown files" is elegant in its simplicity, there are many other ways to create a kind of 'second brain' using some combination of existing tools and technologies. There's now also dozens of companies aiming to solve this need, from startups building dedicated new AI brains and context layers, all the way to established database, ETL and data warehouse companies jumping on the bandwagon. ## Landing on SQLite For any team that's amassed even a moderate amount of historic knowledge, a functional 'company brain' is clearly very valuable. That motivated us to create our own around April. We really liked the simplicity of combining "a folder of markdown files" with the agents of our choice. We had some specific requirements in mind, however, that took us in a somewhat unique direction: 1. We wanted to use structured data where possible (not just unstructured text) with proper querying ability. 2. We wanted the 'brain' to be collaborative: multiple people in the team should be able to access and manage the same knowledge. 3. We wanted something self-hosted rather than putting our sensitive data into a 3rd party SaaS product. Ideally we wanted to use open-source tools. We have been a very pro-SQLite company since our founding, and so in light of our requirements, SQLite seemed like a natural choice as the foundation of the project. We know that agents grep'ing text files works superbly well, so our main question early on was: how well would it work if we *just gave an agent a SQLite database file*? In this post I wanted to share the architecture and results. And let me just preface that by saying: This is a specific opinionated flavor of a company brain that works for us, as a team of about ~30 people. Your mileage may vary! ## TL;DR: What We Built We built a set of simple Node.js-based ingesters that mostly use APIs & Webhooks to aggregate data from 14 (and counting) different systems like Google Drive, GitHub, Zendesk, Gong, HubSpot, Slack, etc. into a single Postgres database. The shape of the data is optimized for agent usability, using Markdown for free text, and rolling up data into columns in a single row containing JSON or Markdown where it makes sense. Then we used PowerSync to sync Postgres with a SQLite database, in which we create a full-text search (FTS) index. We dynamically generate a skill that gives any agent useful information about the schema and SQLite query conventions (including using the `sqlite3` CLI which ships with macOS and is readily available on Linux and Windows). Then we created two tools for the team that can be used based on preference: 1. A Slack bot that has a real-time synced SQLite database (using the PowerSync Node.js SDK), and uses Claude in headless mode with our provided skill to answer prompts when tagged in public Slack channels[^1]; and 2. A CLI tool that provides a synced SQLite database and our skill, so that you can use the company brain on your own machine with the agent of your choice. Since it's just SQLite, you can use it offline with local models. ## TL;DR: The Results This relatively simple architecture resulted in an incredibly useful tool. It turns out that the latest models and harnesses are really good at figuring out how to use a SQLite database. This includes running queries (including aggregations) over embedded JSON in the database – SQLite has really good built-in support for JSON. ![Screenshot of the company brain Slack bot answering a question about PostgreSQL failover support, citing a Zendesk ticket and an internal engineering document.](https://powersync.com/images/blog/building-our-company-brain-agents-sqlite-offline-capable-inline-slack-bot-answer.png) Our team quickly adopted the tool and it has saved us a huge amount of time on doing any kind of work that benefits from stitching together context across siloed systems. Our experience was very similar to what the YCombinator team [spoke about](https://www.youtube.com/watch?v=B246K_G7mHU&t=455s) when they built something similar with Postgres: > It didn't just make it easier to answer questions, it dramatically increased the number of questions that we would ask and dramatically increased the scale and complexity of the questions that we would dare to ask. If you're interested in a deeper dive of how we put it together, read on. ## Data Ingesters Here's a few examples of our ingesters to give you an idea of how we structure the data: * **Slack**: We have one Slack bot that doubles as both a data ingester and a prompt-answerer. It uses Slack's [Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode/) to ingest data in real-time in any channels it's added to, and to listen for prompts. In Postgres, we store one row per channel per day. Whenever there's an event in that channel, we update the existing roll-up for that day. The whole channel conversation is rendered in Markdown format, including the contents of threads. We convert attachments like PDFs and images to Markdown using an LLM API. * **Google Drive:** For simplicity, we currently poll the Drive Changes API. We have a table with a column for the Markdown representation of each Drive file. Google Docs are easy to export as Markdown. We use the Sheets API to build nice Markdown tables of spreadsheets, which frontier LLMs do an excellent job of interpreting. PDFs get converted to Markdown too using an LLM API. For DOC/DOCX files, we use [Mammoth](https://www.npmjs.com/package/mammoth) to convert to HTML, and then convert that to Markdown. We also render all the comments on Drive files as Markdown, including their highlighted text. * **HubSpot:** We ingest all the *Companies* in our HubSpot system (which represent our customers), and embed the contacts for each company as a JSON column on the company row. The HubSpot company IDs are an identifier shared across several of our systems, which makes it easier for agents to associate data across different systems. * **Gong:** We ingest the transcripts of calls, and also use an LLM API to generate meeting notes in Markdown format. * **Stripe:** We ingest customers from Stripe, and build JSON embedded representations of each customer's subscriptions and invoicing history. These are simply stored as columns on the customer rows. * **GitHub:** We ingest Issues, PRs and Discussions – including their descriptions and comments. * **Product Analytics:** We already had a homebrewed product analytics system that stores its data in a Postgres database. When ingesting that data, we transform it into a single row per customer organization, containing a few JSON columns that aggregate useful high-level analytics. * **Gmail:** We ingest customer email threads, by comparing the domain names of the email participants with HubSpot Customers. Email conversations are rendered as Gmail-style threads, with quoted text and non-customer-facing messages stripped. PDF and image attachments are converted to Markdown using an LLM API. There is a "meta" table in Postgres which contains descriptive information about the schema. Each ingester's `schema.sql` definition file is responsible for doing an upsert into that table with its metadata. These help the agent with interpreting the database and knowing where to find what information (more on that below). ![Screenshot of the Postgres "meta" table with columns for table name, source system, description, and column comments.](https://powersync.com/images/blog/building-our-company-brain-agents-sqlite-offline-capable-inline-meta-table.png "The 'meta' table describes the schema so agents know where to find what information.") ## Syncing to SQLite For syncing from Postgres to SQLite, we define auto-subscribing [Sync Streams](https://docs.powersync.com/sync/streams/overview#sync-streams) in PowerSync for the relevant tables. We currently have a single company-wide dataset, but we could also use partial syncing for gating access to specific data. This would also allow scaling to a much larger total dataset – with users only syncing the part of the data that is relevant to them. ```yaml stripe_customers: auto_subscribe: true queries: - SELECT id, stripe_id, email, name, description, currency, balance, delinquent, created_at, subscriptions, invoices FROM stripe_customers WHERE archived = false ``` At our scale we currently have on the order of ~100k records being synced to SQLite, which takes about ~20 seconds for the initial sync, and then PowerSync keeps the SQLite database updated incrementally with deltas after that. ## Skill We dynamically generate a skill (based on a template) containing information to help the agent with using the SQLite database: * A schema description using the contents of the "meta" table mentioned above. * Usage and input/output guidelines for the `sqlite3` CLI * Guidelines on SQLite types, working with JSON, and using full-text search (FTS) * Suggested workflow for answering a question * Basic heuristics to decide what information is authoritative * Matching related data about an organization or person across tables (prefer shared identifiers where available, or use company domain name or email address, or fall back to proper noun matching) * Confidentiality note ("Do not transmit data outside the user's local environment without explicit permission") It might actually be sufficient to have a static skill with instructions on retrieving the schema description from the "meta" table, to avoid the step of dynamically generating the skill. We haven't tested that yet. ## Slack Bot The prompt-answering/Q&A part of the Slack bot is pretty straightforward. It listens for mentions of its name on any channels that it's a member of, and then launches `claude` in headless mode to execute prompts using the skill and SQLite database (which is synced in real-time using the PowerSync Node.js client SDK). If a schema change is detected, it currently resyncs the SQLite database (since this is a reasonably inexpensive operation). For security reasons, we block the Slack bot from answering questions in any Slack Connect channel, or any channel with outside guests. We also limit the tool calls that Claude can execute (`sqlite3`), disable web search/fetch, provide minimal allowlisted env vars, and limit the outbound network access of the machine where the bot runs. ## CLI Tool When the CLI tool is first launched, it opens an internal web app where the user has to sign in with their Google Workspace account (our main identity provider). That passes a longer-lived JWT back to the CLI. The CLI uses that longer-lived JWT with the internal web app's API to securely mint short-lived JWTs (valid for only 5 minutes) for PowerSync's authentication which allows syncing the SQLite database. If the longer-lived JWT expires or its associated session is revoked, the CLI wipes the SQLite database for security. Whenever the CLI is running, it keeps the SQLite database in sync in near real-time (we will probably turn this into a background daemon in the future). Any decent agent can use the SQLite database and skill provided by the CLI. ## It Works Offline, Running Locally On-Device Since we are giving the agent all the context it needs in a SQLite file, we can use the company brain offline with local models. We have used it with Ollama paired with either [Pi](https://pi.dev/) or [Claude Code](https://docs.ollama.com/integrations/claude-code). With models like [Gemma4 26B](https://deepmind.google/models/gemma/gemma-4/) that we've been using so far, the quality of company brain answers are obviously not on par with frontier models like Opus, Fable and GPT5, but are still respectable and provide a solid fallback if we happen to not have Internet access at any point[^2]. When the user goes back online, PowerSync will sync the deltas to bring the SQLite database up to date. This project serves as a reference implementation for how PowerSync can be used to provide a locally-available synced context layer (in SQLite) for "edge AI" / on-device AI use cases with local open-weight models. This was not specifically one of the original requirements for our 'company brain', but it turned out to be a useful benefit of the PowerSync-SQLite architecture. ## Roadmap This is phase 1. There's a lot of things we want to explore further. The first one will likely be bi-directional syncing. We can allow agents to codify knowledge in the database (e.g. customer intel, market knowledge, etc.). PowerSync allows us to control how SQLite mutations are processed into Postgres with server-authoritative logic in our backend application. Another one is keeping fresh copies of all our product Git repos as references that can be consulted when answering questions and performing work. This would give us even richer context and more precision. Agents will be able to combine data from all our systems with the product source code as ground truth. We have several other ideas that are percolating. ## Want to Use This? If you would like to run your own version of this and would like to use our implementation as a baseline, we will open-source it. [Let me know](mailto:conrad@powersync.com) if you're interested. ## Postgres Alternative? Instead of syncing the data from Postgres to SQLite, another possibility we superficially investigated is to define a skill that instructs the agent to query Postgres directly, using [`psql`](https://www.postgresql.org/docs/current/app-psql.html). We briefly tested this but didn't explore it deeply. With the PowerSync-SQLite approach, access control can be achieved by partially syncing the Postgres data to users based on their permissions, whereas for Postgres, [Row-Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) may be suitable for access control. When using Postgres, the 'company brain' would also not be usable offline, and as mentioned above, an on-device AI reference implementation was one of the benefits we achieved with this project. However, using Postgres directly may be suitable for the needs of other companies. [^1]: The experience is similar to [Claude Tag](https://www.anthropic.com/news/introducing-claude-tag), which was launched by Anthropic a few weeks after we launched our internal company brain Slack bot. [^2]: Another idea we want to investigate is using something like [LiteLLM AI Gateway](https://docs.litellm.ai/docs/simple_proxy) to route prompts to cloud or local models based on their complexity. --- # Convex + PowerSync: Design Notes from the Experimental Release > Adding experimental Convex support: the design decisions we made, the parts of Convex that fit well, the rough edges, and the open questions we're still working through. - Published: 2026-06-17 - Author: Kobie Botha - Category: Engineering - Source: https://powersync.com/blog/convex-powersync-design-notes --- We've added experimental [Convex](https://www.convex.dev/) support to PowerSync, after a steady amount of requests for it from the community. This post is the engineering side of that work: the design decisions we made, the parts of Convex that fit PowerSync well, the rough edges, and the questions we're still working through. ## PowerSync in thirty seconds If you haven't used PowerSync before, here's briefly how it works. PowerSync keeps a backend database in sync with a local SQLite database embedded in your app, so the app reads and writes locally, is highly responsive, and keeps working whether or not it's online. There are two main pieces: the PowerSync Service, which runs on the server and connects to your source database, and a client SDK, which manages the local SQLite database on each device. On the read path, the Service sends each client only the rows it should see, based on *Sync Streams* you define. That's partial sync: every device holds its own subset of the data, not a copy of the whole database. On the write path, your app writes to local SQLite, the SDK queues the change, and that queue uploads through a path you control. For Convex, that path is your existing Convex mutations. ## Convex support is just another module PowerSync is designed to be stack-agnostic and each supported source database (currently: Postgres, MongoDB, MySQL, SQL Server, and now Convex) is its own [module](https://github.com/powersync-ja/powersync-service/tree/main/modules) on top of a shared core. The core handles what's common to every backend: the sync protocol, bucket storage (how replicated data is partitioned for efficient syncing), and checkpointing (tracking how far replication has progressed). Each module handles the database-specific replication. So adding Convex support came down to writing the Convex-specific replication code, and that's mostly what the rest of this post covers. ## How the Convex module replicates data PowerSync reads from Convex through its Streaming Export API, using three endpoints: `json_schemas` lists the tables, `list_snapshot` reads a full copy of a table, and `document_deltas` returns changes to data in the order they happened. When PowerSync first connects, it pins a single snapshot timestamp and reads every selected table at that timestamp. Because the timestamp stays fixed, the copy is consistent even though `list_snapshot` reads the tables one at a time. That initial copy is resumable. PowerSync records how far it got in each table, so a restart continues instead of starting over. This matters for large datasets, where the first copy can take a while. Once the copy is done, PowerSync switches to streaming. It polls `document_deltas` for new changes, starting from the same timestamp the snapshot was taken at, so no rows are missed and nothing is copied twice. For each change, it uses your Sync Streams to decide which rows to keep, how to transform them, and which buckets they belong to. Buckets are the partitions PowerSync stores replicated data in, so that later, when a client connects, the Service can hand it just the buckets it needs. ## What fit well A few things about Convex lined up well with how PowerSync already works. A Convex mutation is atomic, and every write in it shares one commit timestamp (`_ts`). Convex never splits those writes across two `document_deltas` pages, so we receive them together and commit them as a single transaction. A client never sees half a mutation. `document_deltas` also returns the complete document after each change, not just the fields that changed. That means we don't have to keep our own copy of the previous row to work out a diff, so we store less per row than we do for some other databases. Another big win was having a stable ID. Convex gives every document an `_id` that never changes. Our Postgres, MySQL and SQL Server replicators all have to track replica identity, because the columns that identify a row are configured per table and a row's identity can change underneath you. With Convex, `_id` is always the identity, so we skipped that whole class of bookkeeping. ## The checkpoint table PowerSync needs to know when a client's queued writes have been acknowledged by Convex. We get that signal from the Convex replication cursor: once replication reaches the cursor recorded for a write checkpoint, we can tell the client its write is durable. The catch is that the `document_deltas` cursor only advances when something writes to Convex. On an idle deployment, polling `document_deltas` returns the same cursor over and over, so a write checkpoint can sit in storage, correct but never delivered, because no later change ever moves replication up to it. Our fix is to write to Convex ourselves. After we record a write checkpoint, we call a `createCheckpoint` mutation that upserts a row in a small `powersync_checkpoints` table. That write produces a new entry in `document_deltas`, which advances the cursor. When the replicator sees the marker row it ignores it as user data, but the new cursor position is enough to release the write checkpoint to the client. You deploy the mutation; we call it. We're not doing anything unusual here, though on other backends this step is generally plumbing that the developer never has to touch. For example, in our Postgres replicator, we emit a logical replication message, which you never see, and with MongoDB we manage a `_powersync_checkpoints` collection for you. Ideally we'd like to remove this step. It would take a way to emit an ordered event into `document_deltas` without running a mutation, or a change to how we publish write checkpoints so we don't need the cursor to advance past an already-committed position. We wrote up our full analysis in [convex-write-checkpoints.md](https://github.com/powersync-ja/powersync-service/blob/main/docs/convex/convex-write-checkpoints.md). ## ID mapping with client-generated UUIDs Convex generates document IDs server-side, so a client can't know a row's `_id` until after it's inserted. PowerSync needs the opposite: a stable local ID the moment a row is created, since the row lives in local SQLite and the app reads and writes it before the upload reaches Convex. To bridge that, each replicated document carries a client-generated UUID, synced back to the client as `uuid AS id`. Convex keeps its own `_id`, the client uses the UUID, and your mutations map between the two on upload. This is the most visible DX cost of the integration today since every mutation has to handle the mapping. We looked into whether Convex could take a client-supplied ID instead, but from our analysis it would need significant changes on the Convex side. For now, it seems the UUID pattern is here to stay. ## Schema changes PowerSync reads each row's value straight from the JSON document in `list_snapshot` and `document_deltas`, not from Convex's `json_schemas` metadata. That's deliberate: `json_schemas` can omit a field until a document populates it, so reading types from it would tie a column's representation to whether Convex had reported that field yet. A nice side effect is that schema changes mostly handle themselves. Adding a field, removing one, or changing a type flows through `document_deltas` as a normal document change, with no re-snapshot. The exception is dropped tables. Deleting a table emits no per-document deletes, so rows already on clients can linger. The workaround is to clear the table first, or delete its documents through a mutation. We've documented this limitation and plan to handle it properly in a later release. ## Sync Streams instead of Convex server functions The Convex team has been candid that they're not fully comfortable with this part of the design, and we think it's worth discussing openly. In a Convex app, reads are TypeScript query functions deployed to Convex. Authorization lives in those functions, and Convex's reactivity pushes updates to subscribed clients. With PowerSync, the read path for synced data moves out of those functions. Which rows sync to each client is defined in [Sync Streams](https://docs.powersync.com/sync/streams/overview) (SQL-like queries the Service evaluates), and client reads run as SQL against local SQLite. Authorization you have in query functions gets expressed again in Sync Streams for the synced data. Sync Streams exist because of partial sync. A query function answers a query over the full database; a Sync Stream defines which subset reaches each device. Something has to declare that subset for offline-first, and server functions don't have a primitive for it today. Writes are also affected. Because the client works against SQLite, values that Convex stores as structured data sync down as their SQLite equivalents. A Convex object field, for example, arrives as JSON text, so a mutation that writes it back has to convert it. The demo app has a [basic example](https://github.com/powersync-community/powersync-convex-todolist-demo/blob/main/src/library/powersync/ConvexDecoder.ts) of this. So the tradeoff here is that you get SQL on the client (including joins), offline reads, and devices that only hold data they're allowed to see. You pay by reworking the read path of an existing app, the biggest piece of migration work, plus some conversion work on writes. New apps have less of this. ## Convex Components: the bigger open question Components are Convex's mechanism for packaging reusable backend modules into a Convex app. What if the entire PowerSync service ran as a Convex component? This is not impossible, but it would be a lot of work for us to implement, and we don't yet know whether the consistency guarantees PowerSync relies on would hold up. Still, if the integration gets traction, we're open to exploring it further. ## Try it out We shipped this early version to find out whether the current approach is good enough and to learn where the biggest gaps are. We want your feedback: it will directly influence whether – and how – this integration evolves. Get started with our [Setup Guide](https://docs.powersync.com/intro/setup-guide). Share your feedback by opening an issue on [GitHub](https://github.com/powersync-ja/powersync-service/issues) or join us in [Discord](https://discord.gg/powersync). --- # PowerSync Changelog: May 2026 > A wave of quality community PRs, open engineering discussions, Sync Streams reaching GA, and foundational work on the Dart and Swift SDKs. - Published: 2026-06-09 - Author: Kobie Botha - Category: Product Update - Source: https://powersync.com/blog/powersync-changelog-may-2026 --- Community PRs have been strong lately, and we're doubling down on open-source transparency by moving more engineering discussions into `#engineering-discussions` on our [Discord](https://discord.com/powersync) server. Thanks to all the contributors! On the product side, highlights were that Sync Streams reached GA, the Dart SDK got a major overhaul, the Swift SDK was rewritten in pure Swift, and you can now search and filter instance logs in the Dashboard. Grab another coffee, here's everything that happened over the past couple of months. ## Product updates shipped **PowerSync Service:** * **Sync Streams are now GA**: Since beta we added global CTEs, type-safe generated client wrappers, better error reporting with source offsets, and a long list of bug fixes. If you're still on Sync Rules, now's the time to migrate. Use the button in the dashboard or run `powersync migrate sync-rules` in the CLI. [Release notes](https://releases.powersync.com/announcements/sync-streams-are-now-generally-available) and [Docs](https://docs.powersync.com/sync/streams/overview). * **Earlier detection of Postgres WAL slot problems**: For Postgres source databases, if a replication slot is lost during initial replication, the Service now catches it early and stops rather than running to completion and failing. More importantly, WAL budget warnings now surface in the Dashboard and diagnostics API before a slot is lost, so you can fix the underlying conditions before they become a problem. [Docs](https://docs.powersync.com/maintenance-ops/replication-lag#postgres). * **HA replication for Pro plan+**: PowerSync Cloud instances on the Pro plan and above now get high-availability replication processes. **Client SDKs:** * **Dart/Flutter SDK v2.0 — consolidated and faster**: `powersync` is now the only package you need. We removed `powersync_core`, `powersync_sqlcipher`, and `powersync_flutter_libs`. Encryption is built in, the native connection pool is Rust-backed for faster queries and multi-isolate support, and web databases now default to OPFS on Chrome and Firefox. Learn more in our feature story below. * **Inspect your database in DevTools (Dart/Flutter)**: v2.1.0 adds a PowerSync tab to Dart & Flutter DevTools. Inspect open `PowerSyncDatabase` instances, view active Sync Streams and sync status, and run queries against the local SQLite database while debugging. [Docs](https://docs.powersync.com/tools/dart-devtools-extension). * **Swift SDK rewritten in pure Swift (v1.14.0)**: Previous versions wrapped a Kotlin XCFramework. The SDK is now native Swift throughout: no Kotlin dependency, smaller binary, better debuggability in Xcode, and typed CRUD payload fields. There are no breaking changes to the public API, but parts of the SDK are a full rewrite so we recommend smoke-testing your queries after upgrading. [Release notes](https://releases.powersync.com/announcements/swift-sdk-1-14-0-now-pure-swift). * **Concurrent reads and faster web writes (Web SDK v1.38.0)**: Added support for `OPFSWriteAheadVFS`, a new synchronous VFS using write-ahead logging. Reads no longer block on writes and multiple read queries run in parallel. Currently Chromium-only (it relies on OPFS's readwrite-unsafe mode). [Docs](https://docs.powersync.com/client-sdks/reference/javascript-web#2-opfs-based-alternatives). * **Attachments and Mac Catalyst for .NET (v0.1.2)**: The .NET SDK gained attachment sync APIs matching the implementation in other SDKs, plus Mac Catalyst support. [Changelog](https://releases.powersync.com/announcements/powersync-net-sdk). * **Improved raw tables across all SDKs**: `put` and `delete` statements are now inferred automatically. You only need to provide the `tableName`. Local-only columns and other table options now work with raw tables too. [Docs](https://docs.powersync.com/client-sdks/advanced/raw-tables). * **Capacitor SDK reached beta**: Production-ready for tested use cases and covered by our SLAs. New since alpha: Swift Package Manager support for iOS (requires Capacitor 8+), and significantly faster sync on native platforms via NDJSON-HTTP. [Docs](https://docs.powersync.com/client-sdks/reference/capacitor). **Tools:** * **Search and filter instance logs**: The Logs view in the PowerSync Dashboard now supports free-text search and structured `alias:value` filters (by `user_id`, `client_id`, error code, and more), plus CSV export. [Release notes](https://releases.powersync.com/announcements/searchable-instance-logs-in-the-powersync-dashboard) and [Docs](https://docs.powersync.com/maintenance-ops/monitoring-and-alerting#instance-logs). * **Self-service Private Endpoint setup**: Private Endpoints (AWS PrivateLink) can now be created and managed directly in the PowerSync Dashboard — no support ticket needed. Available on Team and Enterprise plans for Postgres and MongoDB Atlas. [Release notes](https://releases.powersync.com/announcements/self-service-private-endpoints-in-the-dashboard) and [Docs](https://docs.powersync.com/configuration/source-db/setup/private-endpoints). * **Trigger compaction from the CLI**: `powersync compact` triggers compaction on your linked PowerSync Cloud instance directly, with an optional `--timeout` flag for large datasets. [Docs](https://docs.powersync.com/tools/cli). **New demos & guides:** * [Self-hosting on AWS EKS](https://docs.powersync.com/maintenance-ops/self-hosting/aws-eks) — step-by-step guide for deploying PowerSync on Kubernetes via Helm Charts. * [Dart/Flutter DevTools extension guide](https://docs.powersync.com/tools/dart-devtools-extension) — how to use the new database inspector for Dart/Flutter apps. * [Replication lag guide](https://docs.powersync.com/maintenance-ops/replication-lag) — what this metric means, common causes, and remediation steps. * [Diagnosing sync latency](https://docs.powersync.com/debugging/troubleshooting#diagnosing-sync-latency) — how to isolate which stage of the sync process is slow, and how to correlate user reports with sync session logs. ## Community feed * We added a [PowerSync add-on to the TanStack CLI](https://github.com/TanStack/cli/pull/407). Run `create powersync-app --framework react --add-ons powersync` to get a working PowerSync setup for React. * Steven built a [MikroORM + PowerSync PoC](https://github.com/powersync-community/mikro-orm-poc). [Here's the discussion](https://github.com/powersync-ja/powersync-js/discussions/896) if you have thoughts or want to help take it further. * Dev wrote a series on the role sync engines can play in AI apps: [most AI chat apps throw away in-flight responses on refresh or disconnect — treating LLM output as synced state fixes that](https://powersync.com/blog/most-ai-chat-apps-are-broken-sync-engines-are-the-fix), [why AI apps default to single-player and how sync engines bring multiplayer collaboration](https://powersync.com/blog/why-is-every-ai-app-single-player), and a [two-part practical guide](https://powersync.com/blog/building-ai-powered-apps-part-1) on building collaborative, synced AI apps ([Part 2](https://powersync.com/blog/building-ai-powered-apps-part-2)). * Simon wrote up [Easy Tauri Integration Tests with Vitest](https://powersync.com/blog/tauri-integration-tests) — useful if you're building with the Tauri SDK. * New engineering proposals: * **Write API** — Christiaan is exploring a first-class write protocol for PowerSync, covering mutators, conflict handling, and how writes flow from client to backend. This would be a significant change to how you build with PowerSync and we want input from people actually building with it. Join the [discussion in Discord](https://discord.com/channels/1138230179878154300/1504213613009834145/1504851899806126201). * **Raw Table High Performance Diffs** — trigger-based diffing specifically optimized for raw tables, where the current approach has limitations. [Proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit). * **Faster and incremental `sync_local`** — a more internal proposal, with the goal to make initial sync faster and eventually incremental, so it can report progress and avoid blocking writes for minutes on large datasets. [Discussion #178](https://github.com/powersync-ja/powersync-sqlite-core/discussions/178). * **Streaming attachments** — the attachment queue currently loads the entire file into JS memory, which can be a problem with large files. [Discussion #968](https://github.com/powersync-ja/powersync-js/discussions/968). * [@austinbhale](https://github.com/austinbhale): thank you for building out attachments support for the .NET SDK, bringing it to parity with our other SDKs! [PR #68](https://github.com/powersync-ja/powersync-dotnet/pull/68). * Thank you also to [@Shamyyoun](https://github.com/powersync-ja/powersync-kotlin/pull/352), [@sravan27](https://github.com/powersync-ja/powersync-service/pull/644), [@VIVAAN-DHAWAN](https://github.com/powersync-ja/powersync-js/pull/970), [@Sagbyy](https://github.com/powersync-ja/powersync-kotlin/pull/348), [@johnnysedh3lllo](https://github.com/powersync-ja/powersync-js/pull/925), and [@JexanJoel](https://github.com/powersync-ja/powersync-js/pull/924) for your contributions. * Built with PowerSync: * _Fig_ by the team at [figwealth.io](https://figwealth.io): freelancer finance — expense tracking, invoicing, P&L, and tax estimates. [App Store](https://apps.apple.com/us/app/fig-finance-for-freelancers/id6478242586) and [Google Play](https://play.google.com/store/apps/details?id=io.figwealth.app). * _Thunderbolt_ by the team behind Mozilla: an open-source, self-hostable enterprise AI client with chat, search, and research workflows across devices. [Blog post](https://www.thunderbolt.io/blog/mozilla-introduces-thunderbolt). * _Capubridge_ by [@aybinv7](https://github.com/aybinv7): a Tauri desktop devtool for debugging WebView-based Android apps, with direct SQLite inspection. Useful for anyone debugging PowerSync sync state on Capacitor or React Native. [GitHub](https://github.com/aybinv7/capubridge). ## Feature story: The Dart SDK gets a proper foundation The Dart/Flutter SDK has in many ways been our flagship SDK. It was the first SDK we shipped, and it's still one of the most widely used. But it had accumulated a fair amount of technical debt: separate packages for core functionality, encryption, and Flutter libs that you had to wire up yourself; a legacy Dart sync client running in parallel with the Rust one; and some platform-specific rough edges we've been working around. On the web in particular, the setup was annoying: manual WASM file copying, and Safari requiring special cross-origin isolation headers just to use storage. v2.0 addresses that. `powersync` is now the only package you need, whether you're building a Flutter app, a CLI tool, or a server-side Dart service. SQLite loading is now automatic through build hooks — you no longer need `powersync_flutter_libs` or a manual initialization step. Encryption is built in. On native, the connection pool is now Rust-backed, which makes queries faster and lets you safely open the same database across isolates or Flutter engines. On the web, new databases now default to the more performant OPFS on Chrome and Firefox. The sync and database workers are also merged into one file now, so there's less to configure and your compiled output is smaller. We're now doing the same kind of work on the JavaScript SDK. A few open proposals and PRs give a sense of where it's headed: [merging the web workers](https://github.com/powersync-ja/powersync-js/discussions/949) into one (same as we did in Dart), [making WebSocket support an optional dependency](https://github.com/powersync-ja/powersync-js/discussions/950) so you don't bundle it if you don't need it, and [dropping js-logger](https://github.com/powersync-ja/powersync-js/pull/966) as a dependency in favor of letting you bring your own. A good open-source SDK should be easy to use and easy to contribute to. We want both, and that's what inspired this. That's it for this issue. --- # The Journey of Rebuilding Our Dashboard > How we rebuilt the PowerSync Dashboard by dogfooding our own sync engine in production. - Published: 2026-06-04 - Author: Manrich van Greunen - Category: Engineering - Source: https://powersync.com/blog/rebuilding-powersync-dashboard --- When we set out to rebuild the PowerSync Dashboard, we decided it would be more than a technical migration and UI refresh. It was a chance to dogfood our own product in production and share what we learned along the way. This post documents that journey. # Why We Rebuilt: Learning from User Feedback ## Three Goals Driving the Rebuild The most common feedback from users was that the original dashboard felt “confusing and unintuitive” compared to modern tools like Supabase. The original dashboard was built using existing JourneyApps infrastructure and a framework designed for a different use case. The experience was fragmented: features like user account management and billing were relegated to a separate portal, forcing users to jump between tabs just to manage a single organization. Driven by this feedback, we set out with three clear objectives for a new architecture. First, we wanted to replace the IDE-like complexity and problematic UX with something simpler. Second, we committed to dogfooding our own sync engine to power the dashboard’s data layer. Finally, we wanted this project to serve as a best practices reference for our community, demonstrating how to build a multi-tenant SaaS application with PowerSync on the web. ## Backend & API Architecture ### Our Existing Backend The PowerSync Cloud control-plane is composed of **CRUD/RPC-style APIs** that are backed by our existing **MongoDB** databases. ### First Iteration of the New Dashboard The new dashboard was built using a conventional Single Page Application (SPA) stack. This included **React 19** as the core library, **Vite** for efficient bundling, and **Turborepo** for managing the monorepo structure. For client-side routing, we first chose **React Router 7**, and the user interface was built with **shadcn/ui** components. Data fetching and mutation handling against the backend APIs were managed using **TanStack Query**, with **Axios** serving as the primary HTTP client. ## PowerSync Dogfooding Using PowerSync for the dashboard required us to engage with the same documentation, use the same APIs, and confront the same decisions that our users face. A critical decision to make when building with sync engines is defining sync boundaries. For the dashboard, we chose a **hybrid architecture** that plays to the strengths of both sync and traditional APIs. ### Decision: Read-Path Only Sync The dashboard manages a sophisticated hierarchy of entities: **Users, Organizations, Plans, Projects, Instances, Operations, Logs, and Metrics**. Our backend provides APIs for mutations on these entities, but the read-path was where we saw the most potential for improvement with a sync engine. The dashboard **read path** includes data that is read frequently but changed relatively rarely. Because the dashboard is responsible for managing critical infrastructure, we require server-side validation and auditing. While the PowerSync **write path** offers optimistic updates, the resulting complexity of handling asynchronous rollbacks and server-side validation can be significant. Therefore, we decided to initially go with the simpler API-first **write path** that provides synchronous server-side validation and auditing. ### What We Sync (And What We Don’t) The current iteration syncs **Operations** and **Instance Configurations** to SQLite, which include **Sync Config** definitions. Entities like **User Profiles** and **Plans**, which are currently distributed across our microservices, remain on standard API calls, with a plan to unify and integrate them into the sync layer in the future. By contrast, we do not sync high-volume or temporal data like **Logs** and **Metrics**. Sensitive authentication flows and **Billing and** **Usage** operations also remain behind standard API calls. This hybrid approach makes navigating to an instance feel instantaneous because the data is already in the local SQLite database. Loading spinners have effectively vanished from the main navigation flow, and the UI updates in real-time. ### From Sync Rules to Sync Streams: A Real-World Evolution The most significant learning came from actually using PowerSync at scale for our own dashboard. Here's how our approach evolved: #### Sync Rules with JWT Parameters Our first iteration utilized the classic [**Sync Rules**](https://docs.powersync.com/sync/rules/overview) system, the method for defining what data is synchronized to the client. We configured it to embed the required `project_id` in the JWT as shown in the configuration below: ```sql bucket_definitions: project_data: parameters: select request.jwt() ->> 'project_id' as project_id data: - SELECT * FROM instances WHERE project_id = bucket.project_id - SELECT * FROM operations WHERE project_id = bucket.project_id ``` This worked, but critically, switching between projects required generating a new JWT, disconnecting and reconnecting to the PowerSync Service, and waiting for the full sync to complete. These delays and loading states were breaking the instant and fluid UI that sync engines are meant to deliver. #### Next Iteration: Sync Streams We migrated to our new [**Sync Streams**](https://docs.powersync.com/sync/streams/overview) system, which provides native support for on-demand syncing. In the Sync Streams implementation, we pass an array of `project_ids`, and check it against the JWT for proper authorization. See [Sync Streams: Using Parameters](https://docs.powersync.com/sync/streams/parameters) for more detail. ```sql streams: instances: query: SELECT * FROM instances WHERE project_id = subscription.parameter('project_id') AND project_id IN auth.parameter('project_ids') operations: query: SELECT * FROM operations WHERE project_id = subscription.parameter('project_id') AND project_id IN auth.parameter('project_ids') # Note: at the time of writing, edition: 3 is available and recommended config: edition: 2 ``` Now, switching projects simply subscribes to a new stream. If a user switches back to a previous project, the data is already in SQLite and appears instantly #### The “Too many parameter query results” issue During migration, we hit an edge case where the service attempted to resolve our `IN` clauses as independent queries, leading to an explosion of intermediate results and a "Too many parameter query results" error. We fixed this by converting the `IN` clause to a static check: ```sql WHERE project_id = subscription.parameter('project_id') AND (subscription.parameter('project_id') IN auth.parameter('project_ids')) ``` This specific insight has directly informed our new [**Sync Streams Compiler**](https://github.com/powersync-ja/powersync-service/pull/451), which automatically optimizes these patterns for all users. ### Dogfooding Insights: Local-Only Tables While building the Sync Config page, we needed a way to store local drafts as users were editing their Sync Config. We realized that we can unify our data layer and simplify state management by using a **Local-Only** table. In this way we can use the same `useQuery` hooks for both synced cloud data and temporary local drafts. For reference here is the table definition: ```ts export const DraftSyncStreamsTable = { tableDefinition: sqliteTable('draft_sync_streams', { id: text('id').primaryKey(), instance_id: text('instance_id'), valid: integer({ mode: 'boolean' }), definition: text('definition'), created_at: integer('created_at', { mode: 'timestamp' }), updated_at: integer('updated_at', { mode: 'timestamp' }) }), options: { localOnly: true } }; ``` And the `useDraftSyncStreamQuery` hook: ```ts import { db } from '~/lib/powersync'; import { useQuery } from '@powersync/tanstack-react-query'; import { toCompilableQuery } from '@powersync/drizzle-driver'; export const useDraftSyncStreamQuery = ({ instanceId }: SyncStreamVariable) => { const query = toCompilableQuery( db.select().from(DraftSyncStreamsTable).where(eq(DraftSyncStreamsTable.id, instanceId)).limit(1) ); return useQuery({ queryKey: `${instanceId}-drafts`, query }); }; ``` ## Modern Web Stack: The TanStack Router Pivot Story We started with React Router 7, but a hackday exploration of [TanStack Router](https://tanstack.com/router/latest) changed our trajectory. After migrating just two routes, we decided to switch. We get file-based routing that makes sense and type safety that makes it nearly impossible to break a link. TanStack Router with its great TypeScript developer experience provides us 100% autocompletion for route paths and search parameters. It provides flexible route declarations of nested, layout or pathless routes. With automatic prefetching, it makes navigation feel local. We were really impressed and migrated the entire dashboard in a day. ## Development Velocity Through AI & Process ### v0 for Wireframes to Initial UI We used [v0](https://v0.app/) to bridge the gap between wireframes and code. By feeding wireframes into v0, we generated shadcn-based components that got us most of the way to the finish line, allowing us to focus our energy on logic rather than CSS. ### Context Engineering with `AGENTS.md` AI assistance is only as good as the context you provide. Maintaining a strict `AGENTS.md` file not only ensures that every AI output adheres to our technical standards (like strict TypeScript types and accessibility patterns) but also significantly increases development velocity by ensuring code consistency and reducing review time. Here is a snippet of our AI guidelines: ``` # AI Assistant Guidelines ## Core Development Philosophy - **KISS**: Keep It Simple, Stupid - **YAGNI**: You Aren't Gonna Need It ## TypeScript Configuration (STRICT REQUIREMENTS) - **NEVER use `any` type** - use `unknown` if type is truly unknown - **MUST have explicit return types** for all functions - **MUST use `ReactElement` instead of `JSX.Element`** for React 19 ## Component Guidelines - **MAXIMUM 200 lines** per component file - **MUST handle ALL states**: loading, error, empty, and success - **MUST verify actual prop names** before using components ## TanStack Query Patterns - Query keys: Hierarchical factory pattern - Mutations: Automatic cache invalidation - Prefetch hooks for performance-critical paths ## Form Components - **MUST use shadcn/ui Field components** for accessibility - **Required Pattern**: `Field` + `FieldLabel` + `FieldDescription` + `FieldError` - **Validation**: Zod schemas with `.catch()` for graceful failures ``` ## Deployment: Managing Environment Variables Shipping a modern SPA to multiple environments using a single Docker image presented a classic challenge: how to inject environment variables without rebuilding the image. Standard Vite builds bake variables into the bundle at build time, but we wanted a more flexible approach. We used [import-meta-env](https://import-meta-env.org/) to inject these variables at runtime via a simple bash script. We used Node.js's **Single Executable Application** feature to bundle the injection script into a single binary, keeping our deployment pipeline fast and dependency-free. ## Conclusion Rebuilding the PowerSync Dashboard was as much a product exercise as a frontend project, since it was a great chance to dogfood our own sync engine. We are currently working toward the next phase, which includes open-sourcing the dashboard and preparing a version for self-hosted infrastructure. If you have questions about our stack or our architecture, find us on [Discord](https://discord.gg/powersync). --- # Easy Tauri Integration Tests with Vitest > Having a hard time writing integration tests for Tauri? It's very easy with this little trick, even on macOS! - Published: 2026-05-14 - Author: Simon Binder - Category: Engineering - Source: https://powersync.com/blog/tauri-integration-tests --- Recently, we introduced our [PowerSync SDK for Tauri](https://releases.powersync.com/announcements/introducing-the-powersync-tauri-sdk-alpha). The SDK is a Tauri plugin, meaning that it consists of two parts: 1. A Rust crate used to access a local SQLite database, sync changes via PowerSync, and notify JavaScript apps about changes. 2. A small JavaScript library acting as a type-safe wrapper around the raw Tauri IPC commands supported by our Rust crate. As part of the development work, we also looked at ways to test our new SDK. Tauri basically offers [three modes of testing](https://v2.tauri.app/develop/tests/): 1. A [`MockRuntime`](https://docs.rs/tauri/latest/tauri/test/struct.MockRuntime.html) for Rust, allowing us to write unit tests for our Rust crate by mocking out JavaScript. 2. A [fake Tauri environment](https://v2.tauri.app/develop/tests/mocking/) for JavaScript, allowing us to write unit tests for our JavaScript package by mocking out Rust. 3. A [WebDriver](https://v2.tauri.app/develop/tests/webdriver/) integration, allowing us to test everything together. Especially when writing Tauri plugins, options 1 and 2 provide very little value. How our JavaScript and Rust sources interact is the most interesting thing we want to test, as our SDK wouldn't work if anything about that was wrong. Two tests each mocking the other half provide no guarantees. So the only option left available to us was to spin up a demo app and use WebDriver support built into Tauri for integration tests. However, that option has its own big issues: 1. It's designed to test Tauri apps, while we're interested in testing plugin functionality. 2. It doesn't work on macOS, which is what most of us use. In the end, we found a neat way to very reliably test everything in our Tauri plugin nonetheless: Instead of driving an app through WebDriver, what if we launched a Tauri app that simply... tested itself? After all, this is exactly how we test most of our JavaScript nowadays: Instead of mocking web or React APIs to run tests in Node, we let vitest spawn a browser and use the real thing. That loads a web page running our tests, reporting results back to a local vitest server which will print them and set an appropriate exit code for CI. ## Using Tauri as a vitest browser Starting from version 4, vitest has pretty decent support for custom browsers. So our plan to test our SDK was fairly straightforward: 1. Write a tiny Tauri app that allows loading any URL and loads the PowerSync Tauri plugin. 2. Tell vitest to launch that app with a custom URL instead of spawning a browser. You can see the whole thing [in action here](https://github.com/powersync-ja/powersync-js/tree/main/packages/tauri). The `main.rs` for our test app is very simple: It receives a URL as a CLI argument before opening a window with that URL. Using `dev_url` skips some IPC checks and simplifies the setup: ```rust use std::env; use url::Url; fn main() { // Use default options, but open window with URL from args. let mut context = tauri::generate_context!(); if let Some(url) = env::args().skip(1).next() { let config = context.config_mut(); config.build.dev_url = Some(Url::parse(&url).expect("Could not parse URL")); } tauri::Builder::default() .plugin(tauri_plugin_powersync::init()) .run(context) .expect("error while running tauri application"); } ``` For permissions in `capabilities/default.json`, we used these: ```json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "enables the default permissions", "windows": ["*"], "remote": { "urls": ["http://127.0.0.1:*"] }, "permissions": [ "core:default", "powersync:default", "core:webview:allow-create-webview-window", "core:window:allow-set-title" ] } ``` This would not be a good idea for real Tauri apps, but permission checks would just stand in the way for integration tests. To launch this app in vitest, we wrote a custom `BrowserProvider` in `vitest.config.ts`: ```ts const serverFactory = preview().serverFactory; // Relative path to the integration test runner app built with cargo. const testRunnerExecutable = path.resolve('../../target/debug/test-runner'); class TauriBrowserProvider implements BrowserProvider { #tauriApp?: ChildProcess; #isClosing = false; // ... some boring methods omitted async openPage(_sessionId: string, url: string, _options: { parallel: boolean; }) { if (this.#tauriApp != null) { throw new Error('TODO: Calling openPage multiple times is not supported'); } // Ensure the target app spawning webviews is up-to-date. const buildResult = spawnSync('cargo', ['build', '-p', 'test-runner'], { stdio: 'inherit' }); if (buildResult.status !== 0) { throw new Error(`cargo build failed with exit code ${buildResult.status}`); } const app = spawn(testRunnerExecutable, [url]); this.#tauriApp = app; app.on('exit', (code) => { if (!this.#isClosing) { console.log('Test runner exited with code', code); process.exit(1); } }); await new Promise((resolve, reject) => { app.once('spawn', () => resolve()); app.once('error', reject); }); } async close() { this.#isClosing = true; this.#tauriApp?.kill(); } } ``` And that's it! In our `defineConfig` block, we can then use this provider as a custom browser: ```ts export default defineConfig({ test: { include: ['tests/**/*.test.ts'], isolate: false, browser: { enabled: true, provider: { name: 'tauri-app', options: {}, providerFactory() { return new TauriBrowserProvider(); }, serverFactory }, instances: [ // We just need any bogus instance here { browser: 'chrome' } ] }, } }); ``` This is enough to run tests, which now run within a real Tauri app: ![A tauri app running the vitest browser UI](https://powersync.com/images/blog/tauri-test-runner.png) Actually using Tauri APIs requires a small workaround: Tauri installs global definitions into the window which is then used as an IPC hook. Since vitest uses an `