Guides

Realtime

Explain the realtime contract, subscribe and publish flow, and query integration.

Ginjou can work with any realtime tool.

This layer is not a full live-sync data engine. It only sends subscribe and publish notifications. After an event arrives, Ginjou either invalidates active queries or passes the event to your callback.

There is no official backend realtime package in this repo today. This page focuses on the contract and how it connects to the data layer.

mermaid
flowchart LR
	A[Client A useGetList posts] --> B[subscribe]
	B --> C[Realtime Provider]
	D[Client B useCreateOne posts] --> E[publish]
	E --> C
	C --> F[broadcast event]
	F --> A
	A --> G[invalidate or callback]
	G --> H[refetch or manual update]

Realtime Context

Realtime context is the entry point for subscriptions and publish events.

It provides one realtime object for the app.

Interface

interface Realtime {
    subscribe: (props: {
        channel: string
        actions: string[]
        callback: (event: RealtimeEvent<any>) => void
        params?: Record<string, any>
        meta?: Record<string, any>
    }) => string

    unsubscribe?: (key: string) => void

    publish?: (event: RealtimeEvent<any>) => void

    options?: {
        mode?: 'off' | 'auto' | 'manual'
        callback?: (event: RealtimeEvent<any>) => void
        params?: Record<string, unknown>
    }
}

interface RealtimeEvent<TPayload = unknown> {
    channel: string
    action: string
    payload: TPayload
    date: Date
    meta?: Record<string, any>
}

Methods

Method or fieldWhat it does
subscribeStart one subscription and return an unsubscribe key.
unsubscribeClean up one subscription by key.
publishEmit one realtime event.
options.modeSet the default subscription behavior.
options.callbackAdd a shared callback for subscription events.
options.paramsAdd shared provider-specific params to subscriptions.

Global realtime options only include mode, callback, and params.

channel is still chosen per subscription.

Example

<script setup lang="ts">
import { defineRealtime, RealtimeMode } from '@ginjou/core'
import { defineRealtimeContext } from '@ginjou/vue'

const subscriptions = new Map<string, () => void>()
const socketClient = createSocketClient()

defineRealtimeContext(defineRealtime({
    subscribe({ channel, callback }) {
        const key = `${channel}-${Date.now()}`
        const off = socketClient.subscribe(channel, callback)

        subscriptions.set(key, off)
        return key
    },
    unsubscribe(key) {
        subscriptions.get(key)?.()
        subscriptions.delete(key)
    },
    publish(event) {
        socketClient.publish(event.channel, event)
    },
    options: {
        mode: RealtimeMode.Auto,
        params: {
            tenantId: 'team-a',
        },
    },
}))
</script>

Subscribe

Subscribe is the read side of the realtime contract.

It is used internally by these query composables: useGetOne, useGetList, useGetMany, useGetInfiniteList, and useCustom.

For resource queries, Ginjou builds standard subscribe params for you.

Param typeUsed byMain fields
listuseGetList, useGetInfiniteListresource, pagination, sorters, filters, meta
oneuseGetOneresource, id, meta
manyuseGetManyresource, ids, meta

useCustom only subscribes when you pass realtime.channel, and it forwards realtime.params as-is.

If you do not pass actions, Ginjou subscribes with RealtimeAction.Any.

ResultMeaning
stop()Stop the watcher and run unsubscribe cleanup.

Auto / Manual Mode

Realtime mode decides what happens after an event reaches a query composable.

In auto mode, Ginjou invalidates active queries for the current resource and then runs the callback. In manual mode, Ginjou skips invalidation and only runs the callback. The default mode is auto.

Mode can come from global realtime options or from the composable realtime prop. The local realtime.mode value overrides the global one.

Callbacks are combined instead of replaced. Shared params are merged too.

<script setup lang="ts">
import { RealtimeMode } from '@ginjou/core'
import { useGetOne } from '@ginjou/vue'
import { ref } from 'vue'

const lastEvent = ref<unknown>()

const orderQuery = useGetOne({
    resource: 'orders',
    id: 1,
    realtime: {
        mode: RealtimeMode.Manual,
        callback(event) {
            lastEvent.value = event
        },
    },
})
</script>

Channel

channel decides which realtime stream the subscription listens to.

For resource queries, the default channel is resources/${resource}. You can override it per composable when your provider needs a more specific channel name.

Global realtime options do not include channel, so channel selection stays local to each subscription.

<script setup lang="ts">
import { RealtimeAction } from '@ginjou/core'
import { useSubscribe } from '@ginjou/vue'

useSubscribe({
    channel: 'tenant-a/orders',
    actions: [RealtimeAction.Updated],
    params: {
        type: 'one',
        resource: 'orders',
        id: 1,
    },
    callback(event) {
        console.log('channel event', event)
    },
})
</script>

Params

params adds provider-specific data to the subscribe call.

This is useful for values such as tenant id, workspace id, or other server-side routing hints. For built-in resource queries, Ginjou starts with the standard list, one, or many params and then spreads realtime.params on top.

Global options.params and local realtime.params are merged, with local values taking priority when keys overlap.

<script setup lang="ts">
import { useSubscribe } from '@ginjou/vue'

useSubscribe({
    channel: 'resources/orders',
    params: {
        type: 'one',
        resource: 'orders',
        id: 1,
        tenantId: 'team-a',
        workspaceId: 'sales',
    },
    callback(event) {
        console.log('params event', event)
    },
})
</script>

Unsubscribe

subscribe returns an unsubscribe key.

If the provider implements unsubscribe, Ginjou uses that key during cleanup. When you call useSubscribe, the returned stop() function stops the watcher and runs that cleanup for you.

<script setup lang="ts">
import { useSubscribe } from '@ginjou/vue'

const subscription = useSubscribe({
    channel: 'resources/orders',
    params: {
        type: 'one',
        resource: 'orders',
        id: 1,
    },
    callback(event) {
        console.log('realtime event', event)
    },
})

function stopRealtime() {
    subscription.stop()
}
</script>

<template>
    <button @click="stopRealtime">
        Stop subscription
    </button>
</template>

Publish

Publish is the write side of the realtime contract.

These mutation composables already publish events for you:

  • useCreateOne, useCreateMany: publish create events after success
  • useUpdateOne, useUpdateMany: publish update events after success
  • useDeleteOne, useDeleteMany: publish delete events after success
  • useCustomMutation: can publish a custom event through its mutation flow

A successful useCreateOne mutation already emits the create event.

<script setup lang="ts">
import { useCreateOne } from '@ginjou/vue'
import { reactive } from 'vue'

const formData = reactive({
    title: '',
})

const { mutateAsync, isPending } = useCreateOne()

async function submit() {
    await mutateAsync({
        resource: 'posts',
        params: {
            title: formData.title,
        },
    })
}
</script>

<template>
    <form @submit.prevent="submit">
        <input v-model="formData.title">
        <button :disabled="isPending">
            {{ isPending ? 'Creating...' : 'Create post' }}
        </button>
    </form>
</template>

Use usePublish when you want to emit an event yourself. If publish is missing, the emit function becomes a safe no-op. When you omit date, Ginjou fills it with the current time.

<script setup lang="ts">
import { RealtimeAction } from '@ginjou/core'
import { usePublish } from '@ginjou/vue'

const publish = usePublish<{ ids: number[] }>()

function notifyPostCreated() {
    publish({
        channel: 'resources/posts',
        action: RealtimeAction.Created,
        payload: {
            ids: [1],
        },
        meta: {
            fetcherName: 'default',
        },
    })
}
</script>

<template>
    <button @click="notifyPostCreated">
        Publish event
    </button>
</template>
Copyright © 2026