Logux is a flexible JS framework to make local-first sync engine with real-time updates, offline-first, CRDT, an optimistic UI.
- Instead of other local-first solutions, it is not a database, but a framework to build sync engines with specific needs of your project.
- No vendor lock-in. It works with any database and in any cloud.
- Great TypeScript support with end-to-end type checking from client to server.
- We thought about many production-ready problems like monitoring, scaling, outdated clients, authentication, rich test API.
- Optional end-to-end encryption.
- Just extra 7 KB in client-side JS bundle.
Ask your questions at community or commercial support.
Sponsored by Evil MartiansUsing Logux Client:
React client
import { syncMapTemplate } from '@logux/client' export type TaskValue = { finished: boolean text: string authorId: string } export const Task = syncMapTemplate<TaskValue>('tasks')
export const ToDo = ({ userId }) => { const tasks = useFilter(Task, { authorId: userId }) if (tasks.isLoading) { return <Loader /> } else { return <ul> {tasks.map(task => <li>{task.text}</li>)} </ul> } }
export const TaskPage = ({ id }) => { const client = useClient() const task = useSync(Task, id) if (task.isLoading) { return <Loader /> } else { return <form> <input type="checkbox" checked={task.finished} onChange={e => { changeSyncMapById(client, Task, id, { finished: e.target.checked }) }}> <input type="text" value={task.text} onChange={e => { changeSyncMapById(client, Task, id, { text: e.target.value }) }} /> </form> } }
Vue client
Using Logux Vuex:
<template> <h1 v-if="isSubscribing">Loading</h1> <div v-else> <h1>{{ counter }}</h1> <button @click="increment"></button> </div> </template> <script> import { computed } from 'vue' import { useStore, useSubscription } from '@logux/vuex' export default { setup () { // Inject store into the component let store = useStore() // Retrieve counter state from store let counter = computed(() => store.state.counter) // Load current counter from server and subscribe to counter changes let isSubscribing = useSubscription(['counter']) function increment () { // Send action to the server and all tabs in this browser store.commit.sync({ type: 'INC' }) } return { counter, increment, isSubscribing } } } </script>
Pure JS client
You can use Logux Client API with any framework:
client.type('INC', (action, meta) => { counter.innerHTML = parseInt(counter.innerHTML) + 1 }) increase.addEventListener('click', () => { client.sync({ type: 'INC' }) }) loading.classList.add('is-show') await client.sync({ type: 'logux/subscribe' channel: 'counter' }) loading.classList.remove('is-show')
Using Logux Server:
addSyncMap<TaskValue>(server, 'tasks', { async access (ctx, id) { const task = await Task.find(id) return ctx.userId === task.authorId }, async load (ctx, id, since) { const task = await Task.find(id) if (!task) throw new LoguxNotFoundError() return { id: task.id, text: ChangedAt(task.text, task.textChanged), finished: ChangedAt(task.finished, task.finishedChanged), } }, async create (ctx, id, fields, time) { await Task.create({ id, authorId: ctx.userId, text: fields.text, textChanged: time, finished: fields.finished, finishedChanged: time }) }, async change (ctx, id, fields, time) { const task = await Task.find(id) if ('text' in fields) { if (task.textChanged < time) { await task.update({ text: fields.text, textChanged: time }) } } if ('finished' in fields) { if (task.finishedChanged < time) { await task.update({ finished: fields.finished, finishedChanged: time }) } } } async delete (ctx, id) { await Task.delete(id) } }) addSyncMapFilter<TaskValue>(server, 'tasks', { access (ctx, filter) { return true }, initial (ctx, filter, since) { let tasks = await Tasks.where({ ...filter, authorId: ctx.userId }) return tasks.map(task => ({ id: task.id, text: ChangedAt(task.text, task.textChanged), finished: ChangedAt(task.finished, task.finishedChanged), })) }, actions (filterCtx, filter) { return (actionCtx, action, meta) => { return actionCtx.userId === filterCtx.userId } } })