Jinaga

Local-first web and mobile application framework

Choose your language:

A Jinaga mobile or web app works with local data first. The app or PWA works offline. When the device comes online, the data synchronizes with the server.

But that's just the start. The developer experience is incredible.

Imagine coding a mobile app as if the data were already on the device. Or writing a web app that loads data from the local store, not the server. Imagine not maintaining a REST API.

How much faster could you build your app?

Write code only on the edge using Jinaga. Deploy a network of Jinaga Replicators to move data between devices and back-end services.

Replicators

You will never have to write a REST API for your app. You won't need to maintain a database schema. And you won't need to set up queues or topics to get data to your services.

Learn the concepts

Why a New Model?

Most apps today follow a familiar pattern: store data in a central database, fetch it on the client, and send updates back to the server. That works — until it doesn't.

What if users go offline? What if multiple people update the same record? What if those updates arrive out of order?

Suddenly you’re dealing with sync logic, merge conflicts, and race conditions. Every app becomes a distributed systems problem.

Jinaga takes a different approach.

Instead of updating records in place, Jinaga lets you model your domain as a series of immutable facts. Each fact represents something that happened — a task created, a status changed, a user assigned. Facts never change; new facts are added as new events occur.

From these facts, you define rules that describe what the current state should look like. Jinaga uses those rules to:

  • Resolve updates from multiple users automatically
  • Sync changes across devices
  • Work seamlessly offline
  • Track the full history of changes

This model isn’t just for collaboration — it’s for resilience. It’s a way to build applications where data naturally converges across users and devices, without central coordination or complex conflict resolution.

Get it on NuGet.

dotnet add package Jinaga
dotnet add package Jinaga.Store.SQLite
dotnet add package Jinaga.UnitTest

Facts

A Jinaga data model is made of facts. A fact is immutable. That's why we use C# records to represent them. Use the FactType attribute to give it a type name. Here's an example.

[FactType("Blog.Post")]
public record Post(DateTime createdAt, Site site) {}

See that Site type? That's another fact. That's how facts are related to each other.

Creating facts

Whenever the user does something, like create a blog post, your app creates a fact. Use the function jinagaClient.Fact to do that.

await jinagaClient.Fact(new Post(
    DateTime.UtcNow,
    site
));

Call the jinagaClient.Fact function within the mobile app whenever you want to save something. It will be saved locally and sent to the server where it will be stored. You don't need a custom API.

Finding facts

To find a set of facts, first write a specification using LINQ.

var postsInSite = Given<Site>.Match((site, facts) =>
    // Find all posts in this site.
    from post in facts.OfType<Post>()
    where post.site == site
    select new
    {
        hash = jinagaClient.Hash(post),
        // For each post, find all titles.
        titles =
            from title in facts.OfType<PostTitle>()
            where title.post == post
            select title.value
    }
);

The specification finds all facts of a certain type related to the given starting point. It then selects the fields you want to use.

The query method returns all facts matching the specification.

const posts = await jinagaClient.Query(postsInSite, site);

Again, run this code in the mobile client. There is no need to set up an API to perform this query on the server.

Displaying facts

A query is a one-time operation. If you want to update the UI every time a post is created, define a view model. We recommend using MVVM Community Toolkit for data binding. Create a view model to represent items in the collection.

public partial class PostHeaderViewModel : ObservableObject
{
    [ObservableProperty]
    private string title = string.Empty;
}

That's the easy part. Now create a view model to present the collection.

public class PostListViewModel
{
    private readonly JinagaClient jinagaClient;
    private IObserver? observer;

    public ObservableCollection<PostHeaderViewModel> Posts { get; } = new();

    public PostListViewModel(JinagaClient jinagaClient)
    {
        this.jinagaClient = jinagaClient;
    }
}

Add a method to load data. Use jinagaClient.Watch to set up an observer. Pass a lambda to the function to add an object to the observable collection. Then return a lambda that removes it again.

public void Load(Site site)
{
    observer = jinagaClient.Watch(postsInSite, site, projection =>
    {
        // When a post is added, create a new view model.
        var postHeaderViewModel = new PostHeaderViewModel();
        projection.titles.OnAdded(title =>
        {
            // When a post title is added, set the property.
            postHeaderViewModel.Title = title;
        });
        Posts.Add(postHeaderViewModel);

        return () =>
        {
            // When a post is removed, remove the view model.
            Posts.Remove(postHeaderViewModel);
        };
    });
}

Create another method to unload the data and stop the observer.

public void Unload()
{
    // Stop observing.
    observer?.Stop();
    observer = null;
    Posts.Clear();
}

Bind your view model to the view as usual. Call Load and Unload in OnAppearing and OnDisappering. When the view model is loaded, it will fetch posts from the server. In addition, the view will update when the user adds a new post.

Subscribing to facts

To have new posts pushed to the mobile app in real time, replace the call to Watch with Subscribe.

public void Load(Site site)
{
    observer = jinagaClient.Subscribe(postsInSite, site, projection =>
    {
        // Same as above.
    });
}

Conclusion

And with this, facts created by one user make their way to other users. You didn't write a custom API. You didn't set up a Web Socket listener. You didn't define a custom database schema.

Jinaga synchronizes immutable facts from client, to server, and back again. It persists them durably, transmits them reliably, and updates the view automatically.

Jinaga is a product of Jinaga LLC.

Michael L Perry, President