Skip to main content
Back to Blog
Update February 12, 2026 · Steven Melendez

Workspaces That Travel, a Free Trial, and Sync That Actually Works

Workspaces got their own storage, plugins, and migration path. A free trial flow went end-to-end. And the sync engine stopped quietly dropping events.


Today was one of those days where three unrelated threads converged into something that feels like a real product milestone. PrivStack now has a self-serve trial, workspaces that can live on cloud drives, and a sync engine that correctly forwards events between three or more peers. Here's what shipped.

Try PrivStack Without a License Key

The setup wizard has a new first screen: Try Free or Sign In. Picking "Try Free" asks for an email, sends a 6-digit verification code, and issues a signed license key on confirmation. Returning users with an active trial skip verification entirely and go straight to setup.

Workspaces Are Real Now

Up until today, a "workspace" in PrivStack was mostly a UI concept — a name and a set of entities. Storage location was a global setting. Plugin activation was global. Switching workspaces meant reloading the same databases from the same directory with different filters.

That changed. Workspaces now own their storage end-to-end:

Each workspace picks where it lives. During creation, you choose Local, Google Drive, iCloud, or a custom directory. The choice is per-workspace — your "Personal" workspace can live locally while "Work" syncs through Google Drive. Changing storage location after the fact performs a live migration: calculate size, copy files, verify integrity, hot-reload the app against the new path, clean up the old location. If anything fails, it rolls back automatically.

Each workspace picks its plugins. New workspaces use a whitelist model. Instead of starting with every plugin and disabling what you don't need, you pick the ones you want during creation. The Dashboard now has a "Workspace Plugins" section with toggle switches so you can change your mind later. Core plugins (Dashboard, Graph) stay active regardless. Disabling a plugin now actually cleans up — it resets the ViewModel and evicts it from the navigation cache, freeing memory instead of just hiding the tab.

Every file path is workspace-scoped. Note images, Whisper speech models, backups, logs — everything resolves relative to the active workspace directory. Switching workspaces means switching the entire file context, not just the database pointer.

The old global storage settings (DataDirectoryType, CustomDataDirectory) are deprecated with a one-time migration that moves your existing workspace to wherever it was configured to live.

The Sync Engine Stopped Lying

This one is less flashy but arguably more important. The sync engine had several bugs that caused it to silently lose events in multi-peer topologies.

The forwarding bug. When peer A receives an event from peer B, that event needs to eventually reach peer C (through A). It wasn't. The sync ledger — the table that tracks "which entities need syncing with which peers" — wasn't being invalidated when events arrived from remote peers. The EventApplicator sets an entity's modified_at to the event's original creation timestamp, which could be older than the sync ledger's synced_at. So the entity looked "already synced" and C never got the update.

The sharing policy bug. PersonalSyncPolicy was accepting all incoming events regardless of sharing rules. If you shared a document with Alice but not Bob, Bob couldn't pull your events (the send path was correctly filtered). But if Bob pushed events about that document to you, you'd accept them. Fixed — on_event_receive now applies the same sharing filter as on_event_send.

The unshare bug. Unsharing the last entity from a peer deleted the peer's key from the sharing map entirely. An empty map means "no selective sharing configured," which the policy interprets as "allow everything." So unsharing the last entity was equivalent to sharing everything.

The discovery race. When a new peer was discovered but no entities existed yet (because the ShareEntity command hadn't been processed), the peer was permanently marked as "already synced." Auto-sync would never trigger for that peer again, even after entities appeared.

Thankfully for these last 3, this is why sharing still isn't enabled yet and I am actively working on personal sharing for our teams features. It wont be enabled till its properly locked in and functional. Security matters and sharing can have big holes.

All four bugs had corresponding test gaps — the tests were saving events to the event store but not materializing entity rows in the entity store. Since entities_needing_sync queries the entity store, the sync was short-circuiting with zero entities and the tests were passing vacuously. 107 sync tests were updated with proper entity materialization helpers, and all pass now.

DuckDB Stopped Eating All Your RAM

DuckDB defaults to claiming ~80% of system memory and all CPU cores per connection. PrivStack opens five DuckDB databases concurrently (entities, events, blobs, vault, datasets). You can do the math on why that was a problem.

Each database now has explicit resource limits:

  • Entity store: 256MB / 2 threads
  • Event store: 128MB / 1 thread
  • Blob store: 128MB / 1 thread
  • Vault: 64MB / 1 thread
  • Datasets: 256MB / 2 threads

Total ceiling: ~832MB instead of "all of it, five times over."

What Else

  • External connections service — a new abstraction for authenticated third-party integrations. GitHub Device Flow is the first, with tokens stored in the encrypted connections vault.
  • Plugin capability cleanup — disabling a plugin now properly unregisters it from the capability broker. Previously, disabled plugins would still appear as providers.
  • Async plugin reinitialization — workspace switching runs plugin teardown and init on a background thread instead of freezing the UI.
  • Orphaned file cleanup — pre-workspace installations could leave stray DuckDB files at the root data directory. They're cleaned up automatically on startup now.