#greentic #multi-tenant #redis

greentic-session

Greentic multi-tenant session manager with in-memory and Redis backends

10 unstable releases (3 breaking)

Uses new Rust 2024

new 0.4.5 Jan 18, 2026
0.4.4 Dec 15, 2025
0.4.0 Nov 15, 2025
0.3.2 Nov 10, 2025
0.1.0 Oct 31, 2025

#419 in Asynchronous

Download history 11/week @ 2025-11-02 42/week @ 2025-11-09 76/week @ 2025-11-16 73/week @ 2025-11-23 122/week @ 2025-11-30 87/week @ 2025-12-07 203/week @ 2025-12-14 157/week @ 2025-12-21 94/week @ 2025-12-28 267/week @ 2026-01-04 196/week @ 2026-01-11

718 downloads per month
Used in 7 crates (3 directly)

MIT license

54KB
1K SLoC

Rust 1K SLoC Shell 200 SLoC // 0.0% comments

greentic-session

Greentic’s session manager persists flow execution state between user interactions. A session is represented by greentic_types::SessionData, which bundles the tenant context, flow identifier, cursor, and serialized execution snapshot so a runner can pause a Wasm flow, wait for input, and resume exactly where it left off.

Highlights

  • Shared schema – The crate reuses the greentic_types definitions for SessionKey, SessionData, TenantCtx, and UserId, ensuring every runtime speaks the same language when storing or resuming a flow.
  • Pluggable stores – The SessionStore trait exposes a compact CRUD surface (create_session, get_session, update_session, remove_session, find_by_user). There is a thread-safe in-memory implementation plus a Redis implementation for multi-process deployments, both selectable via a Redis-free factory.
  • User routing – Each store maintains a secondary map from (env, tenant, team, user) to a SessionKey so inbound activities can be routed to the correct paused flow without the caller needing to supply the opaque session identifier.
  • Deterministic key helpers – The mapping module offers helpers for deriving stable SessionKey values from connector payloads (e.g., Telegram update fields or webhook metadata).

SessionStore API

use greentic_session::{SessionData, SessionResult, SessionStore};
use greentic_types::{SessionCursor, TenantCtx};

pub trait SessionStore {
    fn create_session(&self, ctx: &TenantCtx, data: SessionData) -> SessionResult<SessionKey>;
    fn get_session(&self, key: &SessionKey) -> SessionResult<Option<SessionData>>;
    fn update_session(&self, key: &SessionKey, data: SessionData) -> SessionResult<()>;
    fn remove_session(&self, key: &SessionKey) -> SessionResult<()>;
    fn find_by_user(
        &self,
        ctx: &TenantCtx,
        user: &UserId,
    ) -> SessionResult<Option<(SessionKey, SessionData)>>;
}

All stores persist the full SessionData blob so the runner can deserialize the exact execution context it previously saved. When find_by_user returns a result, the runner can call update_session with the resumed snapshot (or remove_session once the flow completes).

Quickstart

use greentic_session::{create_session_store, SessionBackendConfig, SessionResult, SessionStore};
use greentic_types::{EnvId, FlowId, SessionCursor, SessionData, TenantCtx, TenantId, UserId};

fn demo() -> SessionResult<()> {
    let store = create_session_store(SessionBackendConfig::InMemory)?;
    let env = EnvId::try_from("dev")?;
    let tenant = TenantId::try_from("tenant-42")?;
    let user = UserId::try_from("user-7")?;
    let ctx = TenantCtx::new(env, tenant).with_user(Some(user.clone()));

    let snapshot = SessionData {
        tenant_ctx: ctx.clone(),
        flow_id: FlowId::try_from("support.flow")?,
        cursor: SessionCursor::new("node.wait_input".to_string()),
        context_json: "{\"ticket\":123}".into(),
    };

    let key = store.create_session(&ctx, snapshot.clone())?;
    let hydrated = store.get_session(&key)?.expect("session present");
    assert_eq!(hydrated.cursor.node_pointer, "node.wait_input");

    let (found_key, _) = store.find_by_user(&ctx, &user)?.expect("user session");
    assert_eq!(found_key, key);

    store.remove_session(&key)?;
    Ok(())
}

Run the example with cargo run --example quickstart to see the same flow end-to-end.

Choosing a backend

Construct a store with the Redis-free configuration enum (no Redis types in the public API and no downstream redis dependency):

use greentic_session::{create_session_store, SessionBackendConfig};

let store = create_session_store(SessionBackendConfig::RedisUrl(
    "redis://127.0.0.1/",
))?;
Feature flag combo Backend availability Suggested usage
default (no flags) In-memory only Tests, single-node dev
--features redis Redis + in-memory Production runners
--all-features Redis + schema docs CI / documentation generation

The Redis backend stores each SessionData blob as JSON under greentic:session:session:{session_key} and maintains user lookup keys at greentic:session:user:{env}:{tenant}:{team}:{user}. When a session is removed, the lookup entry is cleared so new activities fall back to creating a fresh session.

Tenant context enforcement is strict: env, tenant, and team must always match between the caller’s TenantCtx and the stored SessionData, and a stored user (when present) must match the caller. If the stored user is absent, lookups may still be keyed by a caller-provided user without mutating the stored context.

Deterministic Session Keys

  • mapping::telegram_update_to_session_key(bot_id, chat_id, user_id)
  • mapping::webhook_to_session_key(source, subject, id_hint)

Both helpers derive a SHA-256 digest and hex-encode it, making it safe to hash stable identifiers without leaking PII. Use them when the connector should resume the same session even if the runtime doesn’t issue a SessionKey (e.g., webhooks that only provide conversation IDs).

Development

cargo fmt
cargo clippy --all-targets -- -D warnings
cargo test --all-features

Redis tests honor the REDIS_URL environment variable. If unset, the Redis-specific tests are skipped automatically.

Toolchain: Rust 1.89.0 (tracked via rust-toolchain.toml and CI workflows).

Dependencies

~6–25MB
~347K SLoC