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.
Logux Docs is built by Evil Martians, an American design and engineering consultancy for developer tools, AI, and cybersecurity startups.
Using 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
}
}
})