This is the full developer documentation for Actionbase
# Introduction
> Introduction to Actionbase
Actionbase is a database for serving user interactions in real time, production-proven across Kakao services.
Follows, likes, recent views, related content — they are all **interaction data**. Every interaction is expressed in the same model — **who** did **what** to which **target** (*source → action → target*) — and the combination of source and target yields three axes:
* **User–User (U2U)** — follow/unfollow, follower/following counts, timeline scans
* **User–Item (U2I)** — likes/bookmarks, view history, bidirectional counters
* **Item–Item (I2I)** — related products, similar-content graphs, and other precomputed item-to-item relations
Actionbase materializes read-optimized structures at write time. Reads use bounded access patterns (GET, COUNT, SCAN) without expensive computation.
When backed by HBase, Actionbase inherits durability and horizontal scalability.
## Focus
[Section titled “Focus”](#focus)
| Focuses on | Explicitly avoids |
| --------------------------------------------- | ------------------------------------ |
| Real-time interactions across U2U / U2I / I2I | General-purpose graph queries |
| Bounded access patterns (GET, COUNT, SCAN) | Unbounded traversal or analytics |
| Continuous writes, immediate reads | Batch ingestion or deferred indexing |
| WAL/CDC to Kafka (yours or ours) | Owning downstream processing |
| Pluggable storage (HBase now, others planned) | Building yet another storage engine |
## Storage Backend
[Section titled “Storage Backend”](#storage-backend)
Actionbase currently uses HBase as its primary storage backend. Lighter backends are [planned](https://github.com/kakao/actionbase/blob/main/ROADMAP.md#exploring) for smaller deployments.
## Production Usage
[Section titled “Production Usage”](#production-usage)
Used at Kakao services—primarily [KakaoTalk Gift](https://gift.kakao.com/home)—handling over a million requests per minute. Running in stable production for years.
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Quick Start](/quick-start/) — Try Actionbase in minutes
* [Core Concepts](/design/concepts/) — Understand the design
# Quick Start
> Run Actionbase and try core operations in minutes.

## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* Docker
## Start Actionbase
[Section titled “Start Actionbase”](#start-actionbase)
```bash
docker run -it ghcr.io/kakao/actionbase:standalone
```
This runs the server in the background (port 8080) and the CLI in the foreground.
```plaintext
actionbase>
```
Note
The CLI calls REST API internally. Commands show the HTTP requests being made.
## Write Data
[Section titled “Write Data”](#write-data)
Load sample data using a preset (at `actionbase>` prompt):
```plaintext
load preset likes
```
This creates a `likes` database/table and inserts 3 edges:
```plaintext
│ 3 edges inserted
│ - Alice → Phone
│ - Bob → Phone
│ - Bob → Laptop
```
```plaintext
Alice --- likes ----> +--------+
| Phone |
Bob ----- likes ----> +--------+
|
| +--------+
+-- likes ----> | Laptop |
+--------+
```
At write time, Actionbase **precomputes everything** for reads—counts, indexes, ordering for **both directions**. This means you can instantly query:
“What did Bob like?” (direction: OUT)
“Who liked Phone?” (direction: IN)
Counts and indexes ready at write time—no computation at query time.
REST API equivalent
> To use curl, run with `-p 8080:8080` and use another terminal:
>
> ```bash
> docker run -it -p 8080:8080 ghcr.io/kakao/actionbase:standalone
> ```
**Create service (database)**
```bash
curl -X POST "http://localhost:8080/graph/v2/service/likes" \
-H "Content-Type: application/json" \
-d '{"desc":"Likes"}'
```
**Create label (table)**
```bash
curl -X POST "http://localhost:8080/graph/v2/service/likes/label/likes" \
-H "Content-Type: application/json" \
-d '{
"desc":"Like",
"type":"INDEXED",
"schema":{
"src":{"type":"STRING"},
"tgt":{"type":"STRING"},
"fields":[{"name":"created_at","type":"LONG","nullable":false}]
},
"dirType":"BOTH",
"storage":"datastore://likes/likes",
"indices":[{"name":"recent","fields":[{"name":"created_at","order":"DESC"}]}]
}'
```
**Insert edges**
```bash
curl -X POST "http://localhost:8080/graph/v3/databases/likes/tables/likes/edges" \
-H "Content-Type: application/json" \
-d '{
"mutations":[
{"type":"INSERT","edge":{"version":1737377177245,"source":"Alice","target":"Phone","properties":{"created_at":1737377177245}}},
{"type":"INSERT","edge":{"version":1737377177297,"source":"Bob","target":"Phone","properties":{"created_at":1737377177297}}},
{"type":"INSERT","edge":{"version":1737377177350,"source":"Bob","target":"Laptop","properties":{"created_at":1737377177350}}}
]
}'
```
## Read Data
[Section titled “Read Data”](#read-data)
### Get
[Section titled “Get”](#get)
Check if a specific edge exists:
```plaintext
get --source Alice --target Phone
```
```plaintext
│ The edge is found: [Alice -> Phone]
│ |---------------|--------|--------|---------------------------|
│ | VERSION | SOURCE | TARGET | PROPERTIES |
│ |---------------|--------|--------|---------------------------|
│ | 1737377177245 | Alice | Phone | created_at: 1737377177245 |
│ |---------------|--------|--------|---------------------------|
```
REST API equivalent
```bash
curl "http://localhost:8080/graph/v3/databases/likes/tables/likes/edges/get?source=Alice&target=Phone"
```
### Scan
[Section titled “Scan”](#scan)
**What did Bob like?** (direction: OUT)
```plaintext
scan --index recent --start Bob --direction OUT
```
```plaintext
│ The 2 edges found (offset: -, hasNext: false)
│ |---|---------------|--------|--------|---------------------------|
│ | # | VERSION | SOURCE | TARGET | PROPERTIES |
│ |---|---------------|--------|--------|---------------------------|
│ | 1 | 1737377177350 | Bob | Laptop | created_at: 1737377177350 |
│ | 2 | 1737377177297 | Bob | Phone | created_at: 1737377177297 |
│ |---|---------------|--------|--------|---------------------------|
```
**Who liked Phone?** (direction: IN)
```plaintext
scan --index recent --start Phone --direction IN
```
```plaintext
│ The 2 edges found (offset: -, hasNext: false)
│ |---|---------------|--------|--------|---------------------------|
│ | # | VERSION | SOURCE | TARGET | PROPERTIES |
│ |---|---------------|--------|--------|---------------------------|
│ | 1 | 1737377177297 | Bob | Phone | created_at: 1737377177297 |
│ | 2 | 1737377177245 | Alice | Phone | created_at: 1737377177245 |
│ |---|---------------|--------|--------|---------------------------|
```
Note
Both directions are precomputed at write time. No additional indexing or query overhead.
REST API equivalent
```bash
# OUT
curl "http://localhost:8080/graph/v3/databases/likes/tables/likes/edges/scan/recent?start=Bob&direction=OUT"
# IN
curl "http://localhost:8080/graph/v3/databases/likes/tables/likes/edges/scan/recent?start=Phone&direction=IN"
```
### Count
[Section titled “Count”](#count)
**How many items did Alice like?** (direction: OUT)
```plaintext
count --start Alice --direction OUT
```
```plaintext
│ |-------|-----------|-------|
│ | START | DIRECTION | COUNT |
│ |-------|-----------|-------|
│ | Alice | OUT | 1 |
│ |-------|-----------|-------|
```
**How many users liked Phone?** (direction: IN)
```plaintext
count --start Phone --direction IN
```
```plaintext
│ |-------|-----------|-------|
│ | START | DIRECTION | COUNT |
│ |-------|-----------|-------|
│ | Phone | IN | 2 |
│ |-------|-----------|-------|
```
Note
Counts are precomputed at write time. No aggregation needed.
REST API equivalent
```bash
# OUT
curl "http://localhost:8080/graph/v3/databases/likes/tables/likes/edges/count?start=Alice&direction=OUT"
# IN
curl "http://localhost:8080/graph/v3/databases/likes/tables/likes/edges/count?start=Phone&direction=IN"
```
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Core Concepts](/design/concepts/) — How Actionbase works
* [Build Your Social Media App](/guides/build-your-social-media-app/) — Hands-on guide
* [CLI Reference](/operations/cli/) — Full CLI documentation
# Core Concepts
> Core concepts behind Actionbase design
Actionbase is a database for serving user interactions—not a general-purpose graph database.
## Design Goals
[Section titled “Design Goals”](#design-goals)
* **Write-Time Optimization** — Pre-compute read structures at write time. Reads become simple lookups.
* **Leverage Proven Storage** — Build on HBase for durability and scale. Don’t reinvent storage.
## Interaction Axes
[Section titled “Interaction Axes”](#interaction-axes)
Every interaction is expressed in the same model — **who** did **what** to which **target** (*source → action → target*). The combination of source and target yields three axes:
* **User–User (U2U)** — follow/unfollow, follower/following counts, timeline scans
* **User–Item (U2I)** — likes/bookmarks, view history, bidirectional counters
* **Item–Item (I2I)** — related products, similar-content graphs, and other precomputed item-to-item relations
They look different on the surface, but share the same structure: real-time access with predictable query patterns.
## Property Graph Model
[Section titled “Property Graph Model”](#property-graph-model)
Actionbase models interactions as edges:
* **Source**: who or what acts (e.g., user\_id, product\_id)
* **Target**: what is acted on (e.g., product\_id, content\_id, user\_id)
* **Properties**: schema-defined attributes (e.g., `created_at`, `reaction_type`)
```plaintext
User --[likes]--> Product (edge)
├─ source: "user123"
├─ target: "product456"
└─ properties: {
created_at: 1234567890,
reaction_type: "heart"
}
User --[follows]--> User (edge)
├─ source: "user123"
├─ target: "user789"
└─ properties: {
created_at: 1234567891
}
Product --[related]--> Product (edge)
├─ source: "product456"
├─ target: "product789"
└─ properties: {
score: 0.87
}
```
See [Schema](/design/schema/) for defining your edges.
## State and Event Model
[Section titled “State and Event Model”](#state-and-event-model)
Actionbase uses a state-based mutation model:
* **State**: current state (e.g., “user liked product”)
* **Event**: input that transitions state (e.g., “user clicked like”)
When an interaction occurs:
1. Read current state
2. Apply state transition
3. Store new state
Clients attach timestamps to events. Even if events arrive out of order, Actionbase computes the correct final state.
See [Mutation](/design/mutation/) for details.
## Write-Time Optimization
[Section titled “Write-Time Optimization”](#write-time-optimization)
When an edge is written, Actionbase pre-computes:
1. **State** — current relationship between source and target
2. **Index** — ordered structures based on properties (e.g., `created_at DESC`)
3. **Count** — counters (e.g., number of likes per item)
Reads use simple GET, COUNT, SCAN operations without query-time computation.
See [Mutation](/design/mutation/) for how these are created. See [Query](/design/query/) for how to access them.
## Data Flow
[Section titled “Data Flow”](#data-flow)
### Write Path
[Section titled “Write Path”](#write-path)
```plaintext
Client → Server → Engine → WAL → Storage → CDC
```
1. Write to WAL for durability
2. Acquire lock
3. Read current state
4. Apply state transition
5. Compute indexes and counters
6. Write to storage
7. Emit CDC for downstream systems
See [Mutation](/design/mutation/).
### Read Path
[Section titled “Read Path”](#read-path)
```plaintext
Client → Server → Engine → Storage → Response
```
* **COUNT** → EdgeCounter
* **GET** → EdgeState
* **SCAN** → EdgeIndex
See [Query](/design/query/).
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Schema](/design/schema/): Define edge structure
* [Guides](/guides/build-your-social-media-app/): Hands-on tutorial
# Glossary
> Key terminology used in Actionbase documentation
Key terms used in Actionbase documentation.
## Schema Version Mapping
[Section titled “Schema Version Mapping”](#schema-version-mapping)
v2 and v3 map almost 1:1.
| v2 (Current) | v3 (Future) | Description |
| ------------ | ----------- | ----------------------------- |
| service | database | Namespace for grouping tables |
| label | table | Edge schema definition |
| src | source | Who performed the interaction |
| tgt | target | What received the interaction |
| ts | version | Timestamp for ordering |
| fields | properties | Edge attributes |
| dirType | direction | Direction type |
| indices | indexes | Query indexes |
| desc | comment | Description |
* **Schema API**: v2 — migrating to v3
* **Query/Mutation APIs**: v3
## Data Model
[Section titled “Data Model”](#data-model)
| Term | Description |
| --------------- | --------------------------------------------------------------- |
| **Edge** | A relationship representing a user interaction |
| **Source** | Who — the entity performing the interaction (e.g., user\_id) |
| **Target** | What — the entity receiving the interaction (e.g., product\_id) |
| **Properties** | Attributes on an edge (e.g., `created_at`) |
| **Unique-edge** | Edge identified by (source, target) pair |
| **Multi-edge** | Edge identified by ID; multiple per (source, target) |
| **Index** | Pre-computed structure for querying |
| **Alias** | Alternative name for a label/table |
## Query and Mutation
[Section titled “Query and Mutation”](#query-and-mutation)
| Term | Description |
| ------------- | ------------------------------------------------------------- |
| **COUNT** | Query returning number of edges |
| **GET** | Query retrieving edge by source and target |
| **SCAN** | Query scanning edges using pre-computed index |
| **Start** | Starting node for SCAN/COUNT; source when OUT, target when IN |
| **Direction** | `OUT` (source→target) or `IN` (target←source) |
| **Range** | Index-based scan boundaries at storage level |
| **Filter** | Post-retrieval filtering at application level |
| **Version** | Client timestamp (ms/ns) for concurrency and event ordering |
| **Offset** | Encoded pagination position |
| **Limit** | Max results (25 recommended) |
## Write-Time Optimization
[Section titled “Write-Time Optimization”](#write-time-optimization)
| Term | Description |
| --------------- | ---------------------------------------- |
| **EdgeState** | Current edge state, accessed by GET |
| **EdgeIndex** | Sorted index entries, accessed by SCAN |
| **EdgeCounter** | Pre-computed counters, accessed by COUNT |
## Data Pipeline
[Section titled “Data Pipeline”](#data-pipeline)
| Term | Description |
| ------- | --------------------------------------- |
| **WAL** | Write-Ahead Log for durability/recovery |
| **CDC** | Change Data Capture for downstream sync |
# Mutation
> Understanding how mutations work in Actionbase
Mutations insert, update, and delete edges. The process ensures consistency, durability, and write-time optimization.
See [Core Concepts](/design/concepts/) for background.
## Mutation Flow
[Section titled “Mutation Flow”](#mutation-flow)
```
flowchart TD
Request([Mutation Request
with Event, Operation]) --> WAL[Write WAL]
WAL --> Lock[Acquire Lock]
Lock --> Read[Read State
from storage]
Read --> Modify{State exists?}
Modify -->|Yes| ApplyEvent[Apply Event]
Modify -->|No| InitialState[Initial State]
InitialState --> ApplyEvent
ApplyEvent --> ComputeAdditionalInfo[Compute Index, Count
based on changed States]
ComputeAdditionalInfo --> Write[Write State with AdditionalInfo
to storage]
Write --> Release[Release Lock]
Release --> CDC[Write CDC]
CDC --> Response([Response])
```
## Mutation Request
[Section titled “Mutation Request”](#mutation-request)
A mutation request contains:
* **Event**: The data change (e.g., new property values, edge creation)
* **Operation**: Insert, Update, or Delete
## Mutation Process
[Section titled “Mutation Process”](#mutation-process)
### 1. Write WAL
[Section titled “1. Write WAL”](#1-write-wal)
Mutation is written to WAL before changes are made. Enables recovery and replay.
In production, Kafka is used as WAL backend.
### 2. Acquire Lock
[Section titled “2. Acquire Lock”](#2-acquire-lock)
Prevents concurrent modifications:
* **Unique edges**: Lock on (source, target)
* **Multi edges**: Lock on edge ID
### 3. Read State
[Section titled “3. Read State”](#3-read-state)
Read current state from storage—properties, timestamps, metadata.
### 4. Apply Event
[Section titled “4. Apply Event”](#4-apply-event)
Transition state based on operation and client timestamp. See [State Transitions](#state-transitions) for details.
### 5. Compute Indexes and Counters
[Section titled “5. Compute Indexes and Counters”](#5-compute-indexes-and-counters)
Based on changed state:
* **Indexes**: Delete old, create new
* **Counters**: Increment or decrement
### 6. Write to Storage
[Section titled “6. Write to Storage”](#6-write-to-storage)
State, indexes, and counters written atomically.
### 7. Release Lock
[Section titled “7. Release Lock”](#7-release-lock)
Lock released after write.
### 8. Write CDC
[Section titled “8. Write CDC”](#8-write-cdc)
Mutation recorded in CDC (Kafka in production). Resulting state available for downstream systems.
## State Transitions
[Section titled “State Transitions”](#state-transitions)
Edges transition between states based on operations (INSERT, DELETE). Each event carries a client timestamp, and Actionbase uses these timestamps to compute the correct final state—even for out-of-order arrivals and duplicate requests (idempotent).
See [`State.transit`](https://github.com/kakao/actionbase/blob/main/core/src/main/kotlin/com/kakao/actionbase/core/state/StateExtensions.kt) for implementation.
### Diagram
[Section titled “Diagram”](#diagram)
```
flowchart LR
INITIAL[["INITIAL: No Edge"]]
ACTIVE(["ACTIVE: Edge Exists"])
INACTIVE["INACTIVE: Edge Deleted"]
INITIAL -->|"INSERT / +1"| ACTIVE
INACTIVE -->|"INSERT / +1"| ACTIVE
ACTIVE -->|"DELETE / -1"| INACTIVE
```
Full state transition diagram

[Edit on PlantUML](https://www.plantuml.com/plantuml/uml/VPAnRiCW48PtdkAaRbLHcvKXYXKOaAoeIjmkYGTgKpKgmPKDxUlNLiw94JQJBOxlkn-uJUTKw_p5aFx7QP0xMSWi1mQgSkTVpS2UnrgsBUIxc9HSw_MDYwgVodIQaEDZ2PIk9-R3q9AGSO4U7pwCroLTtpiqFwm73c9VdAogAWQhanqQ-Ox1TY-oGl2HHtc0lhtoVWkYBtTKybnC-xQwBcEQYrp4zBZE2S7TwU0HZwbuWAlg6_bq-fZ7_4zrurpY6F0CLlz1qmuVZbAw2ayrOrNTrzKwxsnC3GltI-Gkl22Ck71FGK2PUkxGEec8fT1x3Rau0_4Zf8Se8KXDqG9EDjhM_cB-0G00)
### Example: Out-of-Order Events
[Section titled “Example: Out-of-Order Events”](#example-out-of-order-events)
Alice’s actions: like(t=100) → unlike(t=200) → like(t=300)
Events arrive out of order: like(t=100) → like(t=300) → unlike(t=200)
| # | Event Arrives | State | Count Change | Total |
| - | -------------- | ---------------- | ------------ | ----- |
| 1 | like (t=100) | INITIAL → ACTIVE | +1 | 1 |
| 2 | like (t=300) | ACTIVE → ACTIVE | 0 | 1 |
| 3 | unlike (t=200) | ACTIVE → ACTIVE | 0 | 1 |
Final state: **ACTIVE**, count: **1** — same result regardless of arrival order.
## Write-Time Optimization
[Section titled “Write-Time Optimization”](#write-time-optimization)
During mutations, Actionbase pre-computes:
| Structure | Purpose | Query Type |
| ----------- | ------------------ | ---------- |
| EdgeState | Current edge state | GET |
| EdgeIndex | Sorted entries | SCAN |
| EdgeCounter | Aggregated counts | COUNT |
Reads use simple GET, COUNT, SCAN without query-time computation.
## Consistency Guarantees
[Section titled “Consistency Guarantees”](#consistency-guarantees)
| Mechanism | Guarantee |
| ----------------- | ----------------------------------------- |
| Locking | Prevents concurrent modifications |
| Atomic Writes | State and indexes written together |
| WAL | Durability and recovery |
| Read-Modify-Write | Mutations based on latest state |
| State Transitions | Correct final state despite event arrival |
| Idempotency | Replay produces same result |
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Query](/design/query/): Read pre-computed data
* [Mutation API](/api-references/mutation/): API reference
# Query
> Understanding how queries work in Actionbase
Queries retrieve data pre-computed during mutations.
See [Core Concepts](/design/concepts/) for background.
## Pre-computed Structures
[Section titled “Pre-computed Structures”](#pre-computed-structures)
| Structure | Created During | Accessed By |
| ----------- | -------------- | ----------- |
| EdgeState | Mutation | GET |
| EdgeIndex | Mutation | SCAN |
| EdgeCounter | Mutation | COUNT |
You specify the query type and index. Each query accesses structures prepared at write time.
## Query Types
[Section titled “Query Types”](#query-types)
### GET
[Section titled “GET”](#get)
Retrieves edge state by source and target.
**Use case**: “Has this user viewed this product?”
**Processing**:
1. Construct EdgeState key from source and target
2. Return edge state
**MGet**:
* Multiple source or target IDs → multi-get
* Max 25 edges per request
* Patterns: 1 source with N targets, or M sources with 1 target
### SCAN
[Section titled “SCAN”](#scan)
Scans edges using a pre-computed index with range filtering and pagination.
**Use case**: “Recent products viewed by this user”
**Processing**:
1. Construct EdgeIndex key prefix from source, table, direction, index
2. Apply range filters
3. Scan index entries
4. Apply optional filters
5. Apply pagination (limit, offset)
6. Return matching edges
**Index Requirement**:
* Must specify which index to use
* Index must be defined in schema
### COUNT
[Section titled “COUNT”](#count)
Returns the number of edges for a source node.
**Use case**: “How many products has this user viewed?”
**Processing**:
1. Construct EdgeCounter key from source, table, direction
2. Return pre-computed counter
## Query Flow
[Section titled “Query Flow”](#query-flow)
```
flowchart TD
Request([Query Request]) --> Route{Query Type}
Route -->|GET| GetQuery[Get Query]
Route -->|SCAN| ScanQuery[Scan Query]
Route -->|COUNT| CountQuery[Count Query]
GetQuery --> EdgeState[Read EdgeState]
ScanQuery --> EdgeIndex[Read EdgeIndex]
CountQuery --> EdgeCounter[Read EdgeCounter]
EdgeState --> Response([Response])
EdgeIndex --> Response
EdgeCounter --> Response
```
## Index Ranges
[Section titled “Index Ranges”](#index-ranges)
SCAN queries can specify ranges to filter at storage level.
| Concept | Description |
| -------------- | ----------------------------------------------- |
| Explicit Index | Must specify which index |
| Operators | `eq`, `gt`, `lt`, `between` set scan boundaries |
| Index Order | Ranges applied in field order |
| Sort Direction | Operator meaning depends on ASC/DESC |
### Range vs Filter
[Section titled “Range vs Filter”](#range-vs-filter)
| Type | Level | Uses Index | Performance |
| ------ | ----------- | ---------- | --------------- |
| Range | Storage | Yes | Fast |
| Filter | Application | No | After retrieval |
## Pagination
[Section titled “Pagination”](#pagination)
| Parameter | Description |
| --------- | ---------------------------- |
| offset | Encoded starting position |
| limit | Max results (25 recommended) |
| hasNext | More results available |
## Query Direction
[Section titled “Query Direction”](#query-direction)
| Direction | Description | Example |
| --------- | -------------- | ------------------------- |
| OUT | Outgoing edges | Products a user liked |
| IN | Incoming edges | Users who liked a product |
Separate indexes and counters maintained for each direction.
## Read Path
[Section titled “Read Path”](#read-path)
```plaintext
Client → Server → Engine → Storage → Response
```
1. **Client**: Query via REST API
2. **Server**: Validate request
3. **Engine**: Construct key, retrieve data
4. **Storage**: Return EdgeState/EdgeIndex/EdgeCounter
5. **Response**: Return to client
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Guides](/guides/build-your-social-media-app/): Hands-on tutorial
* [Query API](/api-references/query/): API reference
# Queue
> A partitioned message queue built on immutable edge tables
queue/v1 is a partitioned message queue built on an [immutable edge table](/design/schema/#immutable-edge-tables-type-immutable_indexed-v3-immutable_edge). Messages are ordered within a partition and parallel across partitions, with an explicit lifecycle: **poll → process → commit**.
See the [Queue API Reference](/api-references/queue/) for endpoints.
## Architecture
[Section titled “Architecture”](#architecture)
A queue is an immutable edge table with a fixed schema and a fixed access pattern. Define a queue, and each part is exactly one thing on the underlying [immutable edge table](/design/schema/#immutable-edge-tables-type-immutable_indexed-v3-immutable_edge):
| queue/v1 | Immutable edge table |
| ------------------------------------------ | -------------------------------------------- |
| namespace | database |
| queue | table, type IMMUTABLE\_EDGE |
| partition = `xxhash32(key) mod partitions` | source (LONG) |
| message id | target (STRING) — a server-assigned ULID |
| seq (order / due time) | property (LONG) — the single `seq ASC` index |
| value | property (STRING) — opaque JSON payload |
| enqueue | INSERT one index row (append, lock-free) |
| poll (`seq > offset`, optional `until`) | bounded scan of the `seq ASC` index (OUT) |
| commit (`seq <= offset`) | scan-delete over that index range |
The backing table is append-only and index-only: an enqueue is a single index-row write, and a poll is a single bounded index scan. There is no per-message state, no ack flags, and no consumer registry.
## Partitioning
[Section titled “Partitioning”](#partitioning)
A message is routed by its `key`:
```plaintext
partition = xxhash32(key) mod partitions
```
* Messages with the same key land in the same partition and are read back in `seq` order.
* `partitions` is fixed at queue creation (default 30 = 2·3·5, whose divisors allow many balanced consumer-shard splits).
* Consumers fan out by polling partitions `0 .. partitions-1` independently.
## Consumer Lifecycle
[Section titled “Consumer Lifecycle”](#consumer-lifecycle)
```plaintext
poll(partition, offset) → process the batch → commit(partition, offset = batch offset)
```
1. **Poll** scans one partition forward by `seq`. `offset` is exclusive (`seq > offset`); the response returns the batch and its next `offset` (the highest `seq` seen). An optional `until` bound (inclusive) restricts a poll to due messages when `seq` encodes a due time.
2. **Process** the batch.
3. **Commit** up to the batch’s offset: every message with `seq <= offset` is deleted. There is no separate ack state; the delete is the committed position, and the same operation serves retention.
Caution
Commit deletes a prefix of the partition, so it assumes **one logical consumer per partition** (Kafka-style). Independent consumers sharing a partition would delete each other’s unprocessed messages.
## Delivery Semantics
[Section titled “Delivery Semantics”](#delivery-semantics)
* **At-least-once**: commit only after processing. If a consumer crashes between poll and commit, the next poll re-reads the uncommitted messages.
* **Ordered within a partition**: polls return messages in `seq` order; there is no ordering across partitions.
* **Ordering key**: `seq` is a client-supplied, increasing key (a timestamp, LSN, or sequence number). Uniqueness is not required; the server-assigned ULID `id` keeps same-`seq` messages distinct.
* **Commit is a real delete**: after a commit, re-polling from the start of the partition does not return the committed prefix.
* **No CDC**: immutable edge tables do not emit CDC; the queue itself is the log.
## Use Cases
[Section titled “Use Cases”](#use-cases)
A durable, partitioned, append-only log with a due-time filter — a log primitive, not a full message broker.
| Use case | Fit | Notes |
| ------------------------- | ------- | ----------------------------------------------------------------------------------------- |
| Refresh / delay scheduler | Good | `until` withholds not-yet-due messages; commit clears fired ones |
| Durable per-partition log | Partial | Replay by `offset`; retention is manual via commit (a delete), one consumer per partition |
| Ack-based work queue | No | No per-message ack, visibility timeout, redelivery, or dead-letter |
Not provided: message ack / redelivery, deduplication, independent consumer groups on one partition, and server-side long-poll or streaming.
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Queue API Reference](/api-references/queue/): Endpoints
* [Schema](/design/schema/): Immutable edge tables
* [Mutation API Reference](/api-references/mutation/): The underlying scan-delete primitive
# Schema
> Defining the structure of your interaction data
Schema defines the structure of interaction data. Before storing data, define how edges are structured, what properties they have, and how they can be queried.
See [Core Concepts](/design/concepts/) for background.
## Schema Hierarchy
[Section titled “Schema Hierarchy”](#schema-hierarchy)
Note
Actionbase was open-sourced during the v3 transition. Most docs use v3 terms, but this document uses v2 terms (current Schema API). v3 terms are noted in parentheses. See the last section for full mapping.
```plaintext
Service (v3: Database)
├── Label (v3: Table)
│ ├── Schema (src, tgt, fields)
│ └── Indices (v3: Indexes)
└── Alias
```
* **Service** groups related Labels and Aliases
* **Label** defines the schema for edges
* **Alias** provides an alternative name for a Label
## Service (v3: Database)
[Section titled “Service (v3: Database)”](#service-v3-database)
A namespace that contains labels and aliases.
| Property | Description |
| -------- | -------------------------------------- |
| name | Service identifier (e.g., `myservice`) |
| desc | Description (v3: comment) |
| active | Whether active |
Example: an e-commerce service might contain labels for `likes`, `recent_views`, and `purchases`.
## Label (v3: Table)
[Section titled “Label (v3: Table)”](#label-v3-table)
Defines the schema for edges—src, tgt, fields, and indices.
| Property | Description |
| -------- | ----------------------------------------------------------- |
| name | Format: `service.label` |
| desc | Description (v3: comment) |
| type | Label type (INDEXED, HASH, MULTI\_EDGE, IMMUTABLE\_INDEXED) |
| schema | Edge structure (src, tgt, fields) |
| dirType | Direction type (v3: direction) |
| storage | Storage URI (e.g., `datastore:///
`) |
| indices | Indices for querying (v3: indexes) |
| active | Whether active |
### Schema Definition
[Section titled “Schema Definition”](#schema-definition)
* **src** (v3: source): source type (STRING, LONG) — who
* **tgt** (v3: target): target type (STRING, LONG) — what
* **fields** (v3: properties): each with name, type, nullable
**Example: Recent Views**
```plaintext
src: user_id (LONG)
tgt: product_id (LONG)
fields:
- created_at (LONG)
dirType: BOTH
```
**Example: Reactions**
```plaintext
src: user_id (LONG)
tgt: product_id (LONG)
fields:
- created_at (LONG)
- reaction_type (STRING)
dirType: BOTH
```
### Index Definition
[Section titled “Index Definition”](#index-definition)
Indices enable efficient querying. Each index has a name and a list of fields with sort order.
```plaintext
indices:
- name: by_created_at
fields: [created_at DESC]
- name: by_type_and_time
fields: [reaction_type ASC, created_at DESC]
```
Indices are pre-computed at write time. See [Query](/design/query/).
### Immutable Edge Tables (type: IMMUTABLE\_INDEXED, v3: IMMUTABLE\_EDGE)
[Section titled “Immutable Edge Tables (type: IMMUTABLE\_INDEXED, v3: IMMUTABLE\_EDGE)”](#immutable-edge-tables-type-immutable_indexed-v3-immutable_edge)
An append-only variant of the indexed label. A regular edge tracks mutable state (a like can be un-liked); an immutable edge records facts that never change, such as an event log or a message queue. Only index rows are persisted: there is no state row, no count records, and no caches. The index rows are the record.
| Property | Constraint |
| --------- | ----------------------------------------------- |
| indices | At most one index |
| dirType | Single direction (OUT or IN) — BOTH is rejected |
| mutations | INSERT only — UPDATE and DELETE are rejected |
These constraints ensure every persisted row is reachable by a single index scan, so deleting the scanned rows is a complete delete. That is what makes eviction (scan-delete) safe.
Behavioral differences from a regular indexed label:
| Operation | Behavior |
| --------------- | ---------------------------------------------------------------------------------------------------------- |
| Point get | Rejected; read with an index scan |
| Count / `total` | Not supported; no count records are written |
| CDC | Not emitted; the log itself is the change history |
| Eviction | [Scan-delete](/api-references/mutation/#5-scan-delete-immutable-edge-tables) removes a scanned index range |
**Example: Event Log**
```plaintext
src: partition (LONG)
tgt: event_id (STRING)
fields:
- seq (LONG)
- payload (STRING)
dirType: OUT
indices:
- name: seq_asc
fields: [seq ASC]
```
Immutable edge tables back the [queue/v1 API](/design/queue/).
## Alias
[Section titled “Alias”](#alias)
An alternative name for a label.
| Property | Description |
| -------- | ------------------------- |
| name | Format: `service.alias` |
| desc | Description (v3: comment) |
| target | The label it points to |
| active | Whether active |
Useful for gradual migrations or domain-specific naming.
## Naming Conventions
[Section titled “Naming Conventions”](#naming-conventions)
| Type | Format | Example |
| ------- | ----------------- | ------------------- |
| Service | Simple identifier | `myservice` |
| Label | `service.label` | `myservice.likes` |
| Alias | `service.alias` | `myservice.friends` |
## Schema Versions
[Section titled “Schema Versions”](#schema-versions)
v2 and v3 map almost 1:1.
| v2 (Current) | v3 (Future) |
| ------------ | ----------- |
| service | database |
| label | table |
| src | source |
| tgt | target |
| ts | version |
| fields | properties |
| dirType | direction |
| indices | indexes |
| desc | comment |
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Mutation](/design/mutation/): Write data
* [Query](/design/query/): Query data
* [Metadata API](/api-references/metadata/): API reference
# Storage Backends
> Understanding storage backends in Actionbase
Actionbase abstracts storage through a minimal interface called `Datastore`. Different backends can be integrated.
## Required Operations
[Section titled “Required Operations”](#required-operations)
A storage backend must support:
| Operation | Description |
| -------------- | ------------------------------------------ |
| get | Retrieve value(s) by key(s) |
| delete | Delete a value by key |
| scan | Range scan with prefix, start, stop, limit |
| checkAndMutate | Atomic check-and-mutate for consistency |
| batch | Batch mutations (optional, recommended) |
## Supported Backends
[Section titled “Supported Backends”](#supported-backends)
### HBase
[Section titled “HBase”](#hbase)
Production backend. Distributed, scalable NoSQL on HDFS.
| Characteristic | Description |
| ---------------------- | ------------------------------------- |
| Horizontal Scalability | Shards data across nodes |
| Strong Durability | Data replicated across nodes |
| Low-latency Access | Optimized for random reads and writes |
HBase requires expertise when used directly (row key design, region splitting, cluster management). Actionbase provides a higher-level abstraction with interaction-specific features (State/Index/Count).
See [HBase Operations](/operations/hbase/).
### Memory
[Section titled “Memory”](#memory)
In-memory backend for development and testing.
| Characteristic | Description |
| -------------- | ------------------------- |
| Easy Setup | No configuration required |
| No Persistence | Data lost on server stop |
Ideal for local development and prototyping.
## How Actionbase Uses Storage
[Section titled “How Actionbase Uses Storage”](#how-actionbase-uses-storage)
| Actionbase Operation | Storage Operation | Data Structure |
| -------------------- | ----------------- | --------------------- |
| Get Query | get | EdgeState |
| Scan Query | scan | EdgeIndex |
| Count Query | get | EdgeCounter |
| Mutation (lock) | checkAndMutate | Lock |
| Mutation (write) | batch / put | State, Index, Counter |
| Mutation (cleanup) | delete | Old indexes |
## Choosing a Backend
[Section titled “Choosing a Backend”](#choosing-a-backend)
| Backend | Use Case |
| ------- | ----------- |
| HBase | Production |
| Memory | Development |
Lighter backends are planned for smaller deployments.
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Data Encoding](/internals/encoding/): How data is encoded
* [HBase Operations](/operations/hbase/): HBase configuration
# 404
> Houston, we have a problem. We couldn’t find that page. Check the URL or try using the search bar.
# Community
> Join the Actionbase community — contribute, discuss, and collaborate
Welcome to the Actionbase community. Here you’ll find everything you need to participate in the project.
[Contributing ](https://github.com/kakao/actionbase/blob/main/CONTRIBUTING.md)How to contribute code, docs, and translations
[Governance ](https://github.com/kakao/actionbase/blob/main/GOVERNANCE.md)Project roles and decision-making
[Release Policy ](https://github.com/kakao/actionbase/blob/main/RELEASES.md)Versioning scheme and support policy
[Roadmap ](https://github.com/kakao/actionbase/blob/main/ROADMAP.md)Planned features and improvements
[Code of Conduct ](https://github.com/kakao/actionbase/blob/main/CODE_OF_CONDUCT.md)Community standards and expectations
[Security Policy ](https://github.com/kakao/actionbase/blob/main/SECURITY.md)How to report security vulnerabilities
## Contributors
[Section titled “Contributors”](#contributors)
[](https://github.com/kakao/actionbase/graphs/contributors)
# FAQ
> Frequently asked questions about Actionbase
## General Questions
[Section titled “General Questions”](#general-questions)
### What is Actionbase?
[Section titled “What is Actionbase?”](#what-is-actionbase)
Actionbase is a database for serving user interactions at scale, spanning three axes — user–user (U2U), user–item (U2I), and item–item (I2I).
It models every interaction the same way — **who** did **what** to which **target** — and materializes read-optimized structures at write time. Reads use bounded access patterns (GET, SCAN, COUNT).
Actionbase is not a general-purpose graph database. It focuses on serving user interactions with predictable read patterns.
### Why is it called Actionbase?
[Section titled “Why is it called Actionbase?”](#why-is-it-called-actionbase)
The name reflects what it stores: user actions, modeled as interactions.
### What do “interactions” and “activities” mean in Actionbase?
[Section titled “What do “interactions” and “activities” mean in Actionbase?”](#what-do-interactions-and-activities-mean-in-actionbase)
An interaction captures a user action (like, view, follow) as an explicit **who → what → target** relationship with schema-defined properties.
Internally, interactions are represented as edges in a graph. The terms “interaction” and “activity” are used interchangeably.
### What problems does Actionbase solve?
[Section titled “What problems does Actionbase solve?”](#what-problems-does-actionbase-solve)
* **U2U** — follow relationships, follower/following counts, timelines
* **U2I** — likes, reactions, view history, and their counts
* **I2I** — related products and similar-content relations
Instead of reimplementing indexing, ordering, and counting logic per service, Actionbase pre-computes these at write time.
### When should I NOT use Actionbase?
[Section titled “When should I NOT use Actionbase?”](#when-should-i-not-use-actionbase)
* A single database instance handles your workload
* You need general-purpose graph queries or traversals
* Your team doesn’t have HBase operational experience
* You’re not hitting scaling walls yet
### Is Actionbase production-ready?
[Section titled “Is Actionbase production-ready?”](#is-actionbase-production-ready)
Actionbase has been running in production at Kakao for years—serving Kakao services, primarily KakaoTalk Gift—handling over a million requests per minute.
However, as an open source project, Actionbase is just getting started. Documentation for production deployment (Kubernetes, HBase operations) is still in progress. Early adopters should expect some rough edges.
### What is the history of Actionbase?
[Section titled “What is the history of Actionbase?”](#what-is-the-history-of-actionbase)
Development started at Kakao in 2023. After deployment to KakaoTalk Gift, it was open-sourced in January 2026 — see [Path to Open Source](/project/path-to-open-source/).
## Data Model
[Section titled “Data Model”](#data-model)
### What data model does Actionbase use?
[Section titled “What data model does Actionbase use?”](#what-data-model-does-actionbase-use)
Actionbase uses a property graph model:
* **Source**: who (e.g., user)
* **Target**: what (e.g., product, content)
* **Properties**: schema-defined attributes (e.g., `created_at`, `reaction_type`)
Each interaction type (likes, views, follows) is defined as a separate table with its own schema.
### What is the difference between unique-edge and multi-edge?
[Section titled “What is the difference between unique-edge and multi-edge?”](#what-is-the-difference-between-unique-edge-and-multi-edge)
* **Unique-edge**: One edge per (source, target) pair. Identified by source and target.
* **Multi-edge**: Multiple edges per (source, target) pair. Each edge has a unique `id`.
Unique-edge fits likes, follows, recent views. Multi-edge fits cases like gift records where the same user can send multiple gifts to the same recipient.
> **Note:** Current documentation focuses on unique-edge. Multi-edge documentation will be expanded later.
### Does Actionbase support vertices (entity data)?
[Section titled “Does Actionbase support vertices (entity data)?”](#does-actionbase-support-vertices-entity-data)
Currently, Actionbase focuses on edges (interactions). Entity data like user profiles or product information is typically stored elsewhere (e.g., RDB).
Vertex support is planned for future releases. In the meantime, self-edges (source = target) can be used as a workaround for simple entity storage.
### Does Actionbase support schemas?
[Section titled “Does Actionbase support schemas?”](#does-actionbase-support-schemas)
Yes. Schemas define:
* Source and target identifier types (int, long, string, etc.)
* Interaction properties and their types
Schemas determine which read-optimized structures (indexes, counts) are built at write time — see [Schema](/design/schema/) and [Metadata API](/api-references/metadata/).
### What are some example interaction models?
[Section titled “What are some example interaction models?”](#what-are-some-example-interaction-models)
**Recent views**
* Source: `user_id`
* Target: `product_id`
* Properties: `created_at`
**Reactions**
* Source: `user_id`
* Target: `content_id`
* Properties: `created_at`, `reaction_type`
## Architecture
[Section titled “Architecture”](#architecture)
### How does Actionbase handle writes?
[Section titled “How does Actionbase handle writes?”](#how-does-actionbase-handle-writes)
Actionbase processes interactions using a state-based [mutation](/design/mutation/) model:
1. Read current state
2. Apply incoming interaction as a state transition
3. Persist resulting state and read-optimized structures
Even if events arrive out of order, the final state remains consistent.
### What is write-time optimization?
[Section titled “What is write-time optimization?”](#what-is-write-time-optimization)
Actionbase pre-computes State, Index, and Count structures when edges are written. This enables simple GET, COUNT, SCAN reads without query-time computation.
1. **State** — current relationship between source and target
2. **Index** — ordered structures based on properties (e.g., `created_at DESC`)
3. **Count** — counters (e.g., number of likes per item)
[Core Concepts](/design/concepts/#write-time-optimization).
### How does Actionbase handle out-of-order events?
[Section titled “How does Actionbase handle out-of-order events?”](#how-does-actionbase-handle-out-of-order-events)
Clients attach timestamps to events. Actionbase uses these timestamps to compute the correct final state, regardless of arrival order — see [Mutation](/design/mutation/).
## Storage and Infrastructure
[Section titled “Storage and Infrastructure”](#storage-and-infrastructure)
### What storage backends does Actionbase support?
[Section titled “What storage backends does Actionbase support?”](#what-storage-backends-does-actionbase-support)
Storage is abstracted in Actionbase. Any backend that meets the interface requirements can be plugged in. HBase is the current implementation; lighter backends are planned — see [Storage Backends](/design/storage-backends/).
### What HBase versions are supported?
[Section titled “What HBase versions are supported?”](#what-hbase-versions-are-supported)
Tested with HBase 2.4 and 2.5. No strict version requirements.
### Why use HBase?
[Section titled “Why use HBase?”](#why-use-hbase)
At Kakao, two storage options met the interface requirements at this scale: HBase and Redicoke (Kakao’s distributed KV store with Redis protocol).
Both provide horizontal scalability, durability, and low-latency random reads/writes. We chose HBase because it was better suited for large data migrations via bulk loading.
> **Note:** Bulk loading is part of the pipeline component. The initial open source release focused on Actionbase core; pipeline release is in progress.
## Streaming and Data Pipelines
[Section titled “Streaming and Data Pipelines”](#streaming-and-data-pipelines)
### What are WAL and CDC?
[Section titled “What are WAL and CDC?”](#what-are-wal-and-cdc)
* **WAL (Write-Ahead Log)** — records incoming events as-is for replay and recovery
* **CDC (Change Data Capture)** — records resulting state after mutation for downstream sync
Both are accessible via Kafka consumers.
### How does Actionbase support analytics and pipelines?
[Section titled “How does Actionbase support analytics and pipelines?”](#how-does-actionbase-support-analytics-and-pipelines)
WAL and CDC streams can feed analytics systems, async processors, background jobs, or data migrations.
### Can Actionbase handle high write throughput?
[Section titled “Can Actionbase handle high write throughput?”](#can-actionbase-handle-high-write-throughput)
For high-frequency interactions (e.g., [recent views](/stories/use-cases/kakaotalk-gift-recent-views/)), Actionbase uses async processing via Spark Streaming:
1. Request queued to WAL, response returned immediately
2. Async processor (Spark Streaming) consumes queued WAL entries and sends mutations back to Actionbase
3. Mutations are throttled and applied in background
This minimizes latency while sustaining throughput. Data is typically reflected within tens of milliseconds. Designed to remain stable even when traffic exceeds normal capacity.
> **Note:** The pipeline component is currently internal. Open source release is in progress.
## Comparison
[Section titled “Comparison”](#comparison)
### How does Actionbase compare to Neo4j?
[Section titled “How does Actionbase compare to Neo4j?”](#how-does-actionbase-compare-to-neo4j)
Actionbase is not a general-purpose graph database.
* **Neo4j** — general-purpose graph queries, traversals
* **Actionbase** — bounded access patterns (GET, SCAN, COUNT) for user interactions
### How does Actionbase compare to traditional RDBMS?
[Section titled “How does Actionbase compare to traditional RDBMS?”](#how-does-actionbase-compare-to-traditional-rdbms)
Actionbase is not a replacement for RDBMS.
* **RDBMS** — general-purpose, transactional
* **Actionbase** — specialized for user interactions at scale, with write-time materialization
For most teams, a well-tuned RDBMS handles this fine. Actionbase exists for cases where that stopped being true.
## Use Cases
[Section titled “Use Cases”](#use-cases)
### What are typical use cases for Actionbase?
[Section titled “What are typical use cases for Actionbase?”](#what-are-typical-use-cases-for-actionbase)
* Like buttons and reaction counts
* “Recently viewed” lists
* Follow/following feeds and follower counts
* Related products and similar-content lists
* Per-user interaction histories
When these outgrow your RDBMS—sharding gets painful, caches drift—Actionbase can take over.
## Getting Started
[Section titled “Getting Started”](#getting-started)
### What are the system requirements?
[Section titled “What are the system requirements?”](#what-are-the-system-requirements)
**Local (development)** — see [Quick Start](/quick-start/)
* Java 17
* In-memory storage, no external dependencies
**Production** — documentation in progress, see [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md)
* Java 17
* 4 GB+ memory recommended (scales out horizontally)
* Requires: HBase, Kafka, JDBC-compatible database for metadata (to be consolidated)
### How do I get started?
[Section titled “How do I get started?”](#how-do-i-get-started)
[Quick Start](/quick-start/).
### Do I need to set up HBase separately?
[Section titled “Do I need to set up HBase separately?”](#do-i-need-to-set-up-hbase-separately)
Yes — documentation in progress, see [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md).
Lighter backends are planned for future releases.
### What programming languages are supported?
[Section titled “What programming languages are supported?”](#what-programming-languages-are-supported)
Actionbase provides a REST API. Any language that supports HTTP works — see [Query API](/api-references/query/) and [Mutation API](/api-references/mutation/).
## Contributing
[Section titled “Contributing”](#contributing)
### How can I contribute?
[Section titled “How can I contribute?”](#how-can-i-contribute)
[Contributing](https://github.com/kakao/actionbase/blob/main/CONTRIBUTING.md).
# For RDB Users
> Understanding Actionbase from a relational database perspective
For users familiar with relational databases who want to understand how Actionbase fits alongside an RDB.
## Why Consider Actionbase?
[Section titled “Why Consider Actionbase?”](#why-consider-actionbase)
As services grow, tables storing user interactions—likes, recent views, follows, related items—often hit scaling walls:
* Shard key management and hot entities
* Cross-shard queries
* Cache consistency
Actionbase handles these by modeling interactions as **who** did **what** to which **target**, with write-time materialization on horizontally scalable storage.
## From Tables to Interactions
[Section titled “From Tables to Interactions”](#from-tables-to-interactions)
In an RDB, interaction data often lives in tables like:
* `user_follows` (user–user)
* `user_likes` (user–item)
* `user_views` (user–item)
* `product_related` (item–item)
In Actionbase, these become edges:
* **Source**: who or what acts (e.g., user\_id, product\_id)
* **Target**: what is acted on (e.g., product\_id, user\_id)
* **Properties**: schema-defined (e.g., `created_at`)
Read-optimized structures (indexes, counts) are pre-computed at write time.
## When Actionbase Fits
[Section titled “When Actionbase Fits”](#when-actionbase-fits)
* Interaction tables dominate volume
* Queries focus on listing or counting relationships
* Sharding these tables gets painful
## Using Actionbase with an RDB
[Section titled “Using Actionbase with an RDB”](#using-actionbase-with-an-rdb)
Actionbase complements an RDB, not replaces it.
A common pattern:
1. Transactional and domain data stays in RDB
2. Large-scale interaction data moves to Actionbase
3. Interaction queries served from Actionbase
Start by migrating only the tables that present scaling challenges.
## Example: Mapping a Table
[Section titled “Example: Mapping a Table”](#example-mapping-a-table)
**RDB**
```sql
CREATE TABLE user_product_wish (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(255),
product_id VARCHAR(255),
created_at TIMESTAMP,
visibility VARCHAR(50)
);
```
**Actionbase**
```
graph LR
Alice((Alice)) -->|wishes| Phone((📱 Phone))
Alice -->|wishes| Laptop((💻 Laptop))
Bob((Bob)) -->|wishes| Phone
```
* **Source**: user\_id (STRING)
* **Target**: product\_id (STRING)
* **Properties**: `created_at` (LONG), `visibility` (STRING)
The `id` column is not needed—unique-edges are identified by source and target.
> **Note:** For multi-edge cases (e.g., multiple gifts from the same user to the same recipient), each edge requires a unique `id` — see [FAQ](/faq/#what-is-the-difference-between-unique-edge-and-multi-edge).
Indexes for efficient queries:
* `created_at DESC` — recent wishes
* `visibility, created_at DESC` — filtered by visibility
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Schema](/design/schema/) — Define your edge schema
* [Quick Start](/quick-start/) — Try Actionbase in minutes
# Build Your Commerce App with Live FOMO Counters
> Build FOMO-based commerce features using real-time aggregation
Note
This guide is being prepared.
This guide will demonstrate how to build FOMO (Fear Of Missing Out) based commerce features using Actionbase’s **real-time aggregation** capabilities.
## Features to Implement
[Section titled “Features to Implement”](#features-to-implement)
* “N users viewing now” - real-time viewer count
* “N users wished recently” - time-windowed wish count
* Time-bucketed interaction aggregation (hourly, minutely, etc.)
* Property-based aggregation
## Actionbase Capability: Real-Time Aggregation
[Section titled “Actionbase Capability: Real-Time Aggregation”](#actionbase-capability-real-time-aggregation)
Actionbase supports real-time aggregation of user interactions:
* **Time buckets**: Aggregate by hour, minute, or custom intervals
* **Discrete values**: Aggregate by property values
* **Interaction counts**: Count user interactions in real time
* **Property aggregation**: Sum, count, and other aggregations on edge properties
This enables commerce features like live viewer counts and trending indicators without external caching or batch processing.
## Coming Soon
[Section titled “Coming Soon”](#coming-soon)
This guide will be available in a future release. Start with the [Build Your Social Media App](/guides/build-your-social-media-app/) guide to learn Actionbase’s core functionality first.
# Build Your Social Gifting App
> Build social-commerce gifting features using multi-edge
Note
This guide is being prepared.
This guide will demonstrate how to build social-commerce gifting features using Actionbase’s **multi-edge** capabilities.
## Features to Implement
[Section titled “Features to Implement”](#features-to-implement)
* Gift sending and receiving history
* Multiple gifts between the same users
* Gift statistics and recommendations
## Actionbase Capability: Multi-Edge
[Section titled “Actionbase Capability: Multi-Edge”](#actionbase-capability-multi-edge)
By default, Actionbase edges are **unique-edge**—identified by (source, target) pairs. Multi-edge extends this model:
* **Unique-edge**: One edge per (source, target) pair
* **Multi-edge**: Multiple edges per (source, target) pair, each identified by a unique `id`
Multi-edge is useful when you need to track multiple interactions between the same entities, such as gift records where the same user can send multiple gifts to the same recipient.
## Coming Soon
[Section titled “Coming Soon”](#coming-soon)
This guide will be available in a future release. Start with the [Build Your Social Media App](/guides/build-your-social-media-app/) guide to learn Actionbase’s core functionality first.
# Build Your Social Media App
> Build social features using Actionbase core functionality
Tip
New to Actionbase? Start with [Quick Start](/quick-start/) for a concise introduction.
This guide walks you through building a simple social media application using Actionbase. You will learn how to model and serve activity data for core features such as **follows**, **likes**, and **feeds**.
[](https://github.com/kakao/actionbase/releases/download/examples/hero.webm)
## What You Will Build
[Section titled “What You Will Build”](#what-you-will-build)
By the end of this guide, you will have implemented:
* **Follows** - Users can follow other users
* **Likes** - Users can like posts
* **Feed** - Display posts from followed users with real-time like counts
These features demonstrate the core pattern behind most social applications.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* Docker
* Web browser
## Start the Interactive Guide
[Section titled “Start the Interactive Guide”](#start-the-interactive-guide)
This guide has an interactive component that runs locally.
1. **Start Actionbase with Docker**
```bash
docker run -it -p 9300:9300 ghcr.io/kakao/actionbase:standalone
```
2. **Start the interactive guide**
Once the CLI prompt (`actionbase>`) appears:
```plaintext
guide start hands-on-social
```
3. **Screen layout**
You will see the screen with three panels:
* **Left**: Progress sidebar showing guide steps
* **Center**: Social app UI
* **Right**: CLI terminal and API logs
A popup will walk you through each step. If something goes wrong, click **Restart** in the top-left corner.

4. **Open in your browser**
```plaintext
http://localhost:9300
```
The sections below summarize what you’ll see — you can read through without running the guide, or use them as a reference while following along.
## Follow the Guide
[Section titled “Follow the Guide”](#follow-the-guide)
From here, continue in the **web browser**. The interactive guide walks you through each step.
1. **Welcome**
Welcome to the Actionbase hands-on guide! In this tutorial, you will:
* Build follows
* Build likes
* See your feed

2. **You are @zipdoki**
You will play as **@zipdoki** throughout this tutorial.
**Tip:** Press **Enter** to proceed.

3. **Set Up**
First, let’s load **sample data** so you can focus on building.
* Create database & tables
* Add sample users & posts

4. **Load Sample Data**
Click **Run** to create:
* Database with users
* Posts & likes tables
```bash
load preset build-your-social-app
```

5. **Select Database**
Switch to the `social` database.
* Use database social
```bash
use database social
```

6. **Explore the Data**
In the previous step, we created these tables:
* user\_posts — who posted what
* user\_likes — who liked which post
Browse around before we add new interactions.

7. **Follows**
Let’s build a **follow** feature.
* Create a table
* Add a relationship
* Query it

8. **Create Follows Table**
Create a `user_follows` table.
* Who follows whom

9. **Follow a User**
Make **@zipdoki** follow **@j4rami**. This creates a connection between two users.
* Precomputing count & index
```bash
mutate user_follows --type INSERT --source zipdoki --target j4rami ...
```
curl
```bash
curl -X POST "http://localhost:9300/graph/v3/databases/social/tables/user_follows/edges" \
-H "Content-Type: application/json" \
-d '{
"mutations": [{
"type": "INSERT",
"edge": {
"version": 1737849600000,
"source": "zipdoki",
"target": "j4rami",
"properties": { "createdAt": 1737849600000 }
}
}]
}'
```

10. **Check Follow Status**
Verify the follow exists.
* Query relationship
```bash
get user_follows --source zipdoki --target j4rami
```
curl
```bash
curl "http://localhost:9300/graph/v3/databases/social/tables/user_follows/edges/get?source=zipdoki&target=j4rami"
```

11. **Count Followers**
Get **@j4rami**’s follower count.
* No aggregation
```bash
count user_follows --start j4rami --direction IN
```
curl
```bash
curl "http://localhost:9300/graph/v3/databases/social/tables/user_follows/edges/count?start=j4rami&direction=IN"
```

12. **List Followers**
Get the list of users following **@j4rami**.
* Already indexed
```bash
scan user_follows --start j4rami --index created_at_desc --direction IN
```
curl
```bash
curl "http://localhost:9300/graph/v3/databases/social/tables/user_follows/edges/scan/created_at_desc?start=j4rami&direction=IN"
```

13. **Likes**
Now let’s add **likes**. Same pattern as follows.
* A user interacts with a post

14. **Like a Post**
Make **@zipdoki** like **@j4rami**’s post.
* Precomputing count & index
```bash
mutate user_likes --type INSERT --source zipdoki --target 1 ...
```
curl
```bash
curl -X POST "http://localhost:9300/graph/v3/databases/social/tables/user_likes/edges" \
-H "Content-Type: application/json" \
-d '{
"mutations": [{
"type": "INSERT",
"edge": {
"version": 1737849600000,
"source": "zipdoki",
"target": 1,
"properties": { "createdAt": 1737849600000 }
}
}]
}'
```

15. **Check Like Status**
Verify that **@zipdoki**’s like was recorded.
* Query like status
```bash
get user_likes --source zipdoki --target 1
```
curl
```bash
curl "http://localhost:9300/graph/v3/databases/social/tables/user_likes/edges/get?source=zipdoki&target=1"
```

16. **And More**
Just like follows, you can:
* Count likes
* List who liked a post
Same pattern, same simplicity.

17. **Feed**
Your **feed** now shows:
* Posts from users you follow
* Real like counts
This is the core pattern behind most social apps.

18. **All Done!**
You just built a **feed** with **follows** and **likes** — all powered by Actionbase.
Now try it yourself:
* Follow someone
* Check your feed
* Like a post

19. **Try It Yourself**
Now it’s your turn to explore. The guide is complete, but the app is fully functional:
* **Follow more users** - Search for users and build your network
* **Check your feed** - See posts from people you follow
* **Like posts** - Interact with content in your feed

## Summary
[Section titled “Summary”](#summary)
You just built a feed with follows and likes - all powered by Actionbase.
### What You Learned
[Section titled “What You Learned”](#what-you-learned)
| Concept | Description |
| ------------------ | -------------------------------------------------------------- |
| **Tables** | Create tables with schemas, indexes, and bidirectional queries |
| **Mutations** | Insert edges representing user interactions |
| **Queries** | Get individual edges, count totals, and scan with indexes |
| **Precomputation** | Counts and indexes are computed at write time for fast reads |
### Next Steps
[Section titled “Next Steps”](#next-steps)
* [Quick Start](/quick-start/) — Core operations in minutes
* [CLI Reference](/operations/cli/) — Full CLI documentation
* [Core Concepts](/design/concepts/) — How Actionbase works
### Share Your Feedback
[Section titled “Share Your Feedback”](#share-your-feedback)
Was this guide helpful? Did Actionbase’s concepts make sense? We’d love to hear your thoughts — questions, suggestions, or issues are all welcome.
[Open an issue on GitHub](https://github.com/kakao/actionbase/issues)
### Behind the Scenes
[Section titled “Behind the Scenes”](#behind-the-scenes)
Curious why we built this guide? [Read the story](https://github.com/kakao/actionbase/issues/454).
# Actionbase
> One database for user-user (follows), user-item (likes), and item-item (related items) interactions — precomputed at write time, served as simple lookups
Pre-computed Reads
Writes do the work. Reads return what’s already computed.
U2U · U2I · I2I
One model for user–user, user–item, and item–item interactions.
REST API
Simple writes, fast reads — all via HTTP.
CDC Built-in
Stream changes for downstream pipelines.
Battle-tested
Serving over a million requests per minute at Kakao.
# Encoding
> Internal data encoding and storage details
This document describes how Actionbase encodes and stores data internally. This information is primarily for contributors and those who need to understand the low-level storage format.
For high-level concepts, see [Core Concepts](/design/concepts/).
## Row Types
[Section titled “Row Types”](#row-types)
Actionbase stores edge data using multiple row types in the storage backend. Each row type serves a specific query purpose.
| Row Type | Type Code | Purpose | Query Type |
| ---------- | --------- | ------------------ | ---------- |
| Edge State | -3 | Current edge state | Get |
| Edge Index | -4 | Index entries | Scan |
| Edge Count | -2 | Edge counts | Count |
Immutable edge tables (`IMMUTABLE_INDEXED`) persist Edge Index rows only, with no Edge State and no Edge Count. The index rows are the whole record, so point gets and counts are not supported and deleting a scanned index range is a complete delete.
## Edge State (Type Code: -3)
[Section titled “Edge State (Type Code: -3)”](#edge-state-type-code--3)
Stores the current state of edges for Get queries.
### Key Structure
[Section titled “Key Structure”](#key-structure)
```plaintext
[4-byte hash] + [1-byte + source] + [1-byte + table code] + [1-byte + type code(-3)] + [1-byte + target]
```
### Value Structure
[Section titled “Value Structure”](#value-structure)
| Field | Type | Description |
| ---------- | ------- | ----------------------------------------- |
| active | Boolean | Edge active status |
| version | Long | Edge version |
| properties | Map | Property values with version per property |
| createdAt | Long | Creation timestamp |
| deletedAt | Long | Deletion timestamp |
## Edge Index (Type Code: -4)
[Section titled “Edge Index (Type Code: -4)”](#edge-index-type-code--4)
Stores index entries for Scan queries. Uses **Narrow Row** format for high-cardinality indexes.
### Key Structure
[Section titled “Key Structure”](#key-structure-1)
```plaintext
[4-byte hash] + [1-byte + directed source] + [1-byte + table code] + [1-byte + type code(-4)] +
[1-byte + direction] + [1-byte + index code] + [(1-byte + N) * # index values] + [1-byte + directed target]
```
### Value Structure
[Section titled “Value Structure”](#value-structure-1)
| Field | Type | Description |
| ---------- | ---- | --------------- |
| version | Long | Edge version |
| properties | Map | Property values |
## Edge Count (Type Code: -2)
[Section titled “Edge Count (Type Code: -2)”](#edge-count-type-code--2)
Stores counters for Count queries. Uses the storage backend’s `increment` operation.
### Key Structure
[Section titled “Key Structure”](#key-structure-2)
```plaintext
[4-byte hash] + [1-byte + directed source] + [1-byte + table code] + [1-byte + type code(-2)] + [1-byte + direction]
```
### Value Structure
[Section titled “Value Structure”](#value-structure-2)
| Field | Type | Description |
| ----- | ---- | ----------- |
| count | Long | Edge count |
## Row Key Encoding
[Section titled “Row Key Encoding”](#row-key-encoding)
All row keys follow a common pattern:
```plaintext
[4-byte hash] + [1-byte + source] + [1-byte + table code] + [1-byte + type code] + [additional fields...]
```
| Component | Size | Purpose |
| ------------- | -------- | --------------------------------- |
| Hash | 4 bytes | xxhash32 for region distribution |
| Type Code | 1 byte | Identifies data type (-2, -3, -4) |
| Source/Target | Variable | Prefixed with 1-byte length |
The hash prefix ensures even distribution across HBase regions, preventing hotspots.
## Value Encoding
[Section titled “Value Encoding”](#value-encoding)
Actionbase uses byte headers to maintain type information:
### Format
[Section titled “Format”](#format)
```plaintext
[1-byte type information] + [actual value]
```
### Type Information
[Section titled “Type Information”](#type-information)
| Aspect | Description |
| ---------- | -------------------------------------------------- |
| Type Code | Distinguishes NULL, STRING, INT, FLOAT, JSON, etc. |
| Sort Order | Includes ASC/DESC for index ordering |
| Encoding | Values encoded according to sort order |
### Version Tracking
[Section titled “Version Tracking”](#version-tracking)
| Row Type | Version Scope |
| --------- | -------------------- |
| EdgeState | Version per property |
| EdgeIndex | Version per edge |
## Key Design Principles
[Section titled “Key Design Principles”](#key-design-principles)
### Hash Prefix
[Section titled “Hash Prefix”](#hash-prefix)
The 4-byte xxhash32 prefix ensures:
* Even distribution across storage regions
* Prevention of write hotspots
* Balanced read/write load
### Negative Type Codes
[Section titled “Negative Type Codes”](#negative-type-codes)
Type codes use negative values (-2, -3, -4) to:
* Separate from user data
* Enable efficient key range scanning
* Provide clear type identification
### Length-Prefixed Strings
[Section titled “Length-Prefixed Strings”](#length-prefixed-strings)
All variable-length fields use 1-byte length prefix:
* Enables efficient parsing
* Supports binary-safe encoding
* Allows prefix-based scanning
# llms.txt
> Using Actionbase documentation with LLMs
Copy and paste to start asking questions about Actionbase.
## Chat
[Section titled “Chat”](#chat)
**1. Overview**
```plaintext
Read https://actionbase.io/llms-small.txt
You are an assistant answering questions about Actionbase.
Answer using only the provided Actionbase documentation.
If the documentation does not specify the answer, say so explicitly.
```
**2. Full documentation**
```plaintext
Read https://actionbase.io/_llms-txt/core.txt
You are an assistant answering questions about Actionbase.
Answer using only the provided Actionbase documentation.
If the documentation does not specify the answer, say so explicitly.
```
**3. Full documentation with API reference**
```plaintext
Read https://actionbase.io/_llms-txt/core.txt
and https://actionbase.io/_llms-txt/api.txt
You are an assistant answering questions about Actionbase.
Answer using only the provided Actionbase documentation.
If the documentation does not specify the answer, say so explicitly.
```
## Source
[Section titled “Source”](#source)
Add as source:
```plaintext
https://actionbase.io/llms-full.txt
```
## Files
[Section titled “Files”](#files)
| File | Description |
| -------------------------------------------- | -------------------------------- |
| [`/llms-small.txt`](/llms-small.txt) | Overview |
| [`/_llms-txt/core.txt`](/_llms-txt/core.txt) | Full documentation (without API) |
| [`/_llms-txt/api.txt`](/_llms-txt/api.txt) | API reference only |
| [`/llms-full.txt`](/llms-full.txt) | Everything |
## Feedback
[Section titled “Feedback”](#feedback)
Have tips for better prompts, questions, or suggestions? [Open an issue](https://github.com/kakao/actionbase/issues) to share them.
# Benchmarks
> Performance benchmarks and metrics for Actionbase
Performance benchmarks for Actionbase.
**Note:** At this initial release, benchmark results are approximate. We plan to provide transparent benchmarks using generalized datasets in future releases.
## Production Infrastructure
[Section titled “Production Infrastructure”](#production-infrastructure)
Actionbase runs as stateless services that can scale horizontally:
* **Ingress** (Actionbase): 9 nodes (6 cores, 12GB RAM) — API gateway, request routing
* **Worker** (Actionbase): 9 nodes (40 cores, 64GB RAM) — mutation processing, query execution
* **HBase**: 9 region servers (20 cores, 64GB RAM each) — storage backend
Ingress and Worker are Actionbase deployments. They maintain no local state—all data resides in the storage backend (HBase). This stateless architecture enables:
* **Horizontal scaling**: Add nodes without data migration
* **Rolling updates**: Deploy without downtime
* **Failure recovery**: Any node can handle any request
## Production Traffic
[Section titled “Production Traffic”](#production-traffic)
Current peak: \~33k RPS (2M RPM). Typical workload: \~15% write, \~85% read.
* **Latency**: Read \~5ms, Write \~20ms
## Capacity
[Section titled “Capacity”](#capacity)
60k+ RPS in internal testing.
## Scalability
[Section titled “Scalability”](#scalability)
Linear within tested range.
# CLI
> Command-line interface for interacting with Actionbase
Note
CLI is in preview. Commands and options may change.
The Actionbase CLI provides an interactive console for managing databases, tables, and edges. It connects to an Actionbase server and supports all metadata and data operations.
## Installation
[Section titled “Installation”](#installation)
* Docker (Recommended)
The easiest way to experience Actionbase is using Docker. The standalone image runs the server in the background and the CLI in the foreground:
```bash
docker run -it --pull always ghcr.io/kakao/actionbase:standalone
```
This is sufficient for exploring Actionbase and understanding its core capabilities.
* Homebrew
```bash
brew tap kakao/actionbase https://github.com/kakao/actionbase
brew install actionbase
```
* From Source
```bash
git clone https://github.com/kakao/actionbase.git
cd actionbase/cli
make build
```
The binary is created at `cli/bin/actionbase`.
## Starting the CLI
[Section titled “Starting the CLI”](#starting-the-cli)
```bash
actionbase [options]
```
Tip
When using Docker standalone (`ghcr.io/kakao/actionbase:standalone`), the CLI starts with `--debug --proxy` enabled by default.
### Startup Options
[Section titled “Startup Options”](#startup-options)
| Option | Description | Default |
| ----------------- | ---------------------------------------------------- | ----------------------- |
| `--host ` | Actionbase server URL | `http://localhost:8080` |
| `--authKey ` | Authentication key | (none) |
| `--debug` | Enable debug logging (shows HTTP requests/responses) | off |
| `--plain` | Plain text output mode (no colors) | off |
| `--proxy [port]` | Start in proxy mode for interactive guides | `9300` |
| `--version` | Display CLI version and exit | |
### Examples
[Section titled “Examples”](#examples)
```bash
# Connect to local server
actionbase
# Connect to remote server
actionbase --host https://actionbase.example.com
# Enable debug mode to see HTTP traffic
actionbase --debug
# Start with proxy mode for guides
actionbase --proxy
```
## Context Management
[Section titled “Context Management”](#context-management)
The CLI maintains session context for database and table selection. The prompt displays the current context:
```plaintext
actionbase> # No context
actionbase(mydb)> # Database selected
actionbase(mydb:mytable)> # Database and table selected
```
### context
[Section titled “context”](#context)
Display current session state.
```plaintext
context
```
**Output includes:**
* Current host URL
* Current database
* Current table or alias
* Proxy mode status (on/off with port)
* Debug mode status (on/off)
**Example:**
```plaintext
actionbase> context
│ |----------|--------------------------------|
│ | KEY | VALUE |
│ |----------|--------------------------------|
│ | host | http://localhost:8080 |
│ | database | likes |
│ | table | likes |
│ | alias | |
│ | proxy | off (port -) |
│ | debug | off |
│ |----------|--------------------------------|
```
### use
[Section titled “use”](#use)
Switch the current database, table, or alias context.
```plaintext
use
```
| Subcommand | Description |
| --------------------- | --------------------------------------------------------- |
| `use database ` | Switch to specified database (clears table/alias context) |
| `use table ` | Switch to specified table (requires database context) |
| `use alias ` | Switch to specified alias (requires database context) |
**Examples:**
```plaintext
actionbase> use database likes
│ Current database: likes
actionbase(likes)> use table likes
│ Current table: likes
actionbase(likes:likes)>
```
### debug
[Section titled “debug”](#debug)
Enable or disable debug mode. When enabled, HTTP requests and responses are logged.
```plaintext
debug
```
**Example:**
```plaintext
actionbase> debug on
│ Debugging is on
actionbase> get --source Alice --target Phone
│ → GET /graph/v3/databases/likes/tables/likes/edges/get?source=Alice&target=Phone
│ ← 200 OK {"edges":[...]}
```
## Metadata Operations
[Section titled “Metadata Operations”](#metadata-operations)
### create
[Section titled “create”](#create)
Create databases, storages, tables, or aliases.
#### create database
[Section titled “create database”](#create-database)
```plaintext
create database --name --comment
```
| Flag | Required | Description |
| ----------- | -------- | -------------------- |
| `--name` | Yes | Database name |
| `--comment` | Yes | Database description |
**Example:**
```plaintext
actionbase> create database --name social --comment "Social interactions"
│ Database created: social
```
#### create storage
[Section titled “create storage”](#create-storage)
```plaintext
create storage --name --comment --storageType --hbaseNamespace --hbaseTable
```
| Flag | Required | Description |
| ------------------ | -------- | ------------------- |
| `--name` | Yes | Storage name |
| `--comment` | Yes | Storage description |
| `--storageType` | Yes | Storage type |
| `--hbaseNamespace` | Yes | HBase namespace |
| `--hbaseTable` | Yes | HBase table name |
#### create table
[Section titled “create table”](#create-table)
```plaintext
create table --database --storage --name --comment --type --direction --schema [--indices ] [--groups ]
```
| Flag | Required | Description |
| ------------- | -------- | ------------------------------- |
| `--database` | Yes | Database name |
| `--storage` | Yes | Storage name |
| `--name` | Yes | Table name |
| `--comment` | Yes | Table description |
| `--type` | Yes | Table type |
| `--direction` | Yes | Direction type (IN, OUT, BOTH) |
| `--schema` | Yes | JSON schema definition |
| `--indices` | No | JSON array of index definitions |
| `--groups` | No | JSON array of group definitions |
#### create alias
[Section titled “create alias”](#create-alias)
```plaintext
create alias --database --table
--name --comment
```
| Flag | Required | Description |
| ------------ | -------- | ----------------- |
| `--database` | Yes | Database name |
| `--table` | Yes | Table name |
| `--name` | Yes | Alias name |
| `--comment` | Yes | Alias description |
### show
[Section titled “show”](#show)
Display databases, storages, tables, aliases, indices, or groups.
```plaintext
show [--using
]
```
| Subcommand | Context Required | Description |
| ---------------- | ---------------- | ------------------------------------------- |
| `show databases` | None | List all databases |
| `show storages` | None | List all storages with configuration |
| `show tables` | Database | List tables in current database |
| `show aliases` | Database | List aliases in current database |
| `show indices` | Database + Table | Show indices for current or specified table |
| `show groups` | Database + Table | Show groups for current or specified table |
**Examples:**
```plaintext
actionbase> show databases
│ |-------|---------|
│ | NAME | DESC |
│ |-------|---------|
│ | likes | Likes |
│ | social| Social |
│ |-------|---------|
actionbase(likes)> show tables
│ |-------|------|-----------|
│ | NAME | TYPE | DIRECTION |
│ |-------|------|-----------|
│ | likes | ... | BOTH |
│ |-------|------|-----------|
actionbase(likes:likes)> show indices
│ |------------------|--------|
│ | NAME | FIELDS |
│ |------------------|--------|
│ | created_at_desc | ... |
│ |------------------|--------|
```
### desc
[Section titled “desc”](#desc)
Describe table or alias details including schema, fields, and indices.
```plaintext
desc
[]
```
If `` is omitted, describes the current table or alias.
**Example:**
```plaintext
actionbase(likes)> desc table likes
│ Table: likes
│ Type: INDEXED
│ Direction: BOTH
│
│ Source: STRING
│ Target: STRING
│
│ Fields:
│ |------------|------|----------|
│ | NAME | TYPE | NULLABLE |
│ |------------|------|----------|
│ | created_at | LONG | false |
│ |------------|------|----------|
```
## Data Operations
[Section titled “Data Operations”](#data-operations)
### get
[Section titled “get”](#get)
Query a specific edge by source and target.
```plaintext
get [
] --source --target
```
| Flag | Required | Description |
| ---------- | -------- | --------------------------------------------- |
| `[table]` | No | Table or alias name (uses current if omitted) |
| `--source` | Yes | Source node ID |
| `--target` | Yes | Target node ID |
**Example:**
```plaintext
actionbase(likes:likes)> get --source Alice --target Phone
│ The edge is found: [Alice -> Phone]
│ |---------------|--------|--------|---------------------------|
│ | VERSION | SOURCE | TARGET | PROPERTIES |
│ |---------------|--------|--------|---------------------------|
│ | 1737377177245 | Alice | Phone | created_at: 1737377177245 |
│ |---------------|--------|--------|---------------------------|
```
### scan
[Section titled “scan”](#scan)
Scan edges using an index.
```plaintext
scan [
] --index --start --direction [--ranges ] [--limit ]
```
| Flag | Required | Description |
| ------------- | -------- | --------------------------------------------- |
| `[table]` | No | Table or alias name (uses current if omitted) |
| `--index` | Yes | Index name to scan |
| `--start` | Yes | Starting node ID |
| `--direction` | Yes | Scan direction: IN, OUT, or BOTH |
| `--ranges` | No | Range specification for filtering |
| `--limit` | No | Maximum rows to return (default: 25) |
**Example:**
```plaintext
actionbase(likes:likes)> scan --index created_at_desc --start Alice --direction OUT
│ The 2 edges found (offset: -, hasNext: false)
│ |---|---------------|--------|--------|---------------------------|
│ | # | VERSION | SOURCE | TARGET | PROPERTIES |
│ |---|---------------|--------|--------|---------------------------|
│ | 1 | 1737377177297 | Alice | Laptop | created_at: 1737377177297 |
│ | 2 | 1737377177245 | Alice | Phone | created_at: 1737377177245 |
│ |---|---------------|--------|--------|---------------------------|
```
### count
[Section titled “count”](#count)
Count edges for a specific node and direction.
```plaintext
count [
] --start --direction
```
| Flag | Required | Description |
| ------------- | -------- | --------------------------------------------- |
| `[table]` | No | Table or alias name (uses current if omitted) |
| `--start` | Yes | Starting node ID |
| `--direction` | Yes | Direction: IN, OUT, or BOTH |
**Example:**
```plaintext
actionbase(likes:likes)> count --start Alice --direction OUT
│ |-------|-------|
│ | DIR | COUNT |
│ |-------|-------|
│ | OUT | 2 |
│ |-------|-------|
actionbase(likes:likes)> count --start Phone --direction IN
│ |-------|-------|
│ | DIR | COUNT |
│ |-------|-------|
│ | IN | 2 |
│ |-------|-------|
```
### mutate
[Section titled “mutate”](#mutate)
Insert, update, or delete edges.
```plaintext
mutate [
] --type --source --target --version --properties
```
| Flag | Required | Description |
| -------------- | -------- | -------------------------------------------------- |
| `[table]` | No | Table or alias name (uses current if omitted) |
| `--type` | Yes | Mutation type: INSERT, UPDATE, or DELETE |
| `--source` | Yes | Source node ID |
| `--target` | Yes | Target node ID |
| `--version` | Yes | Timestamp/version (supports `$NOW` placeholder) |
| `--properties` | Yes | JSON object with edge properties (supports `$NOW`) |
**Example:**
```plaintext
actionbase(likes:likes)> mutate --type INSERT --source Charlie --target Phone --version $NOW --properties '{"created_at": $NOW}'
│ Mutation result: 1 updated, 0 failed
```
Tip
Actionbase requires client-issued timestamps for [state transitions](/design/mutation/#state-transitions). Even if events arrive out of order, Actionbase uses these timestamps to compute the correct final state.
Use `$NOW` as a placeholder for the current Unix timestamp in milliseconds. It works in both `--version` and `--properties` values.
## Utility Commands
[Section titled “Utility Commands”](#utility-commands)
### load
[Section titled “load”](#load)
Load and execute commands from a YAML file or preset.
```plaintext
load [--ref ]
```
#### load file
[Section titled “load file”](#load-file)
Load commands from a local YAML file:
```plaintext
load file
```
**YAML file format:**
```yaml
- name: 'Create database'
description: 'Optional description'
command: "create database --name mydb --comment 'My database'"
- name: 'Use database'
command: 'use database mydb'
```
#### load preset
[Section titled “load preset”](#load-preset)
Download and execute a preset from the Actionbase GitHub repository:
```plaintext
load preset [--ref ]
```
| Flag | Required | Description |
| -------- | -------- | --------------------------- |
| `` | Yes | Preset name |
| `--ref` | No | Git branch or tag reference |
**Available presets:**
| Preset | Description |
| ----------------- | ------------------------------------------------- |
| `likes` | Sample likes data (Alice/Bob liking Phone/Laptop) |
| `hands-on-social` | Social media application demo data |
**Example:**
```plaintext
actionbase> load preset likes
│ 3 edges inserted
│ - Alice → Phone
│ - Alice → Laptop
│ - Bob → Phone
```
### guide
[Section titled “guide”](#guide)
Start an interactive guide. Requires proxy mode (`--proxy` flag at startup).
```plaintext
guide start
```
**Available guides:**
| Guide | Description |
| ----------------- | --------------------------------------------- |
| `hands-on-social` | Interactive social media application tutorial |
**Example:**
```bash
# Start CLI with proxy mode
actionbase --proxy
# Then start the guide
actionbase> guide start hands-on-social
```
The guide opens a browser-based interactive tutorial that sends commands to the CLI.
### help
[Section titled “help”](#help)
Display all available commands with descriptions and usage.
```plaintext
help
```
**Example:**
```plaintext
actionbase> help
│ Available commands
│ |---------|-------------------------------|----------------------------------------|
│ | NAME | DESCRIPTION | USAGE |
│ |---------|-------------------------------|----------------------------------------|
│ | context | Show current status | `context` |
│ | create | Create database/storage/... | `create ` |
│ | ... | ... | ... |
│ |---------|-------------------------------|----------------------------------------|
```
### exit
[Section titled “exit”](#exit)
Exit the CLI console.
```plaintext
exit
```
You can also press `Ctrl+C` or `Ctrl+D` to exit.
## Common Workflows
[Section titled “Common Workflows”](#common-workflows)
### Quick Start: Create and Query Data
[Section titled “Quick Start: Create and Query Data”](#quick-start-create-and-query-data)
```plaintext
# 1. Load sample data using preset
actionbase> load preset likes
# 2. Check current context (database and table are set automatically)
actionbase(likes:likes)> context
# 3. Query a specific edge
actionbase(likes:likes)> get --source Alice --target Phone
# 4. Scan all edges from Alice
actionbase(likes:likes)> scan --index created_at_desc --start Alice --direction OUT
# 5. Count edges
actionbase(likes:likes)> count --start Phone --direction IN
```
### Data Operations Workflow
[Section titled “Data Operations Workflow”](#data-operations-workflow)
```plaintext
# 1. Select database and table
actionbase> use database social
actionbase(social)> use table follows
# 2. Insert an edge
actionbase(social:follows)> mutate --type INSERT --source user1 --target user2 --version $NOW --properties '{"created_at": $NOW}'
# 3. Verify the edge
actionbase(social:follows)> get --source user1 --target user2
# 4. Delete the edge
actionbase(social:follows)> mutate --type DELETE --source user1 --target user2 --version $NOW --properties '{}'
```
### Interactive Learning with Guides
[Section titled “Interactive Learning with Guides”](#interactive-learning-with-guides)
```bash
# Start CLI with proxy mode enabled
actionbase --proxy
# Start the interactive guide
actionbase> guide start hands-on-social
```
The guide opens in your browser and walks you through building a social media application.
## Reference
[Section titled “Reference”](#reference)
### Direction Values
[Section titled “Direction Values”](#direction-values)
| Value | Description |
| ------ | -------------------------------- |
| `OUT` | Outgoing edges (source → target) |
| `IN` | Incoming edges (target ← source) |
| `BOTH` | Both directions |
### Mutation Types
[Section titled “Mutation Types”](#mutation-types)
| Type | Description |
| -------- | ----------------------- |
| `INSERT` | Create a new edge |
| `UPDATE` | Update an existing edge |
| `DELETE` | Delete an edge |
### Context Requirements
[Section titled “Context Requirements”](#context-requirements)
| Command | Database Required | Table Required |
| ----------------- | ----------------- | ----------------- |
| `create database` | No | No |
| `create storage` | No | No |
| `create table` | No | No |
| `create alias` | No | No |
| `show databases` | No | No |
| `show storages` | No | No |
| `show tables` | Yes | No |
| `show aliases` | Yes | No |
| `show indices` | Yes | Yes (or specify) |
| `show groups` | Yes | Yes (or specify) |
| `desc table` | Yes | No (uses current) |
| `desc alias` | Yes | No (uses current) |
| `get` | Yes | Yes (or specify) |
| `scan` | Yes | Yes (or specify) |
| `count` | Yes | Yes (or specify) |
| `mutate` | Yes | Yes (or specify) |
### Special Features
[Section titled “Special Features”](#special-features)
**Multi-line input**: End a line with `\` to continue on the next line.
```plaintext
actionbase> create table --database mydb --name mytable \
--comment "My table" --type INDEXED --direction BOTH \
--schema '{"src":{"type":"STRING"},"tgt":{"type":"STRING"}}'
```
**Quote handling**: Input automatically continues until quotes are balanced.
# HBase Configuration
> HBase setup and configuration guide for Actionbase
Note
This guide is being prepared.
Actionbase uses HBase as its primary storage backend. This documentation will include setup instructions, configuration options, and operational best practices.
* To try Actionbase, see [Quick Start](/quick-start/)
* To contribute, see [Development Setup](https://github.com/kakao/actionbase/blob/main/CONTRIBUTING.md#development-setup)
# About Kakao
> Learn about Kakao, the company behind Actionbase
Kakao Corporation (카카오) is a major internet platform company in Korea. [KakaoTalk](https://en.wikipedia.org/wiki/KakaoTalk), its flagship messaging app, has [\~50 million monthly active users](https://t1.kakaocdn.net/kakaocorp/admin/ir/event/5835.pdf)—94% of Korea’s population.
Actionbase was developed at Kakao to handle user interactions at this scale—likes, views, follows—across multiple services. It is now open source.
## Learn More
[Section titled “Learn More”](#learn-more)
* [Kakao Corporation](https://www.kakaocorp.com/)
* [Kakao Tech Blog](https://tech.kakao.com/)
* [Kanana (Kakao AI)](https://github.com/kakao/kanana)
# Path to Open Source
> Actionbase project development history before open source
This document covers the development history of Actionbase before it was open-sourced. Future development will be tracked in public repositories.
## MVP Phase
[Section titled “MVP Phase”](#mvp-phase)
The MVP phase represents the entire period from project inception to open-source release. During this period, Actionbase was applied to various services and evolved through real-world use.
### Phase 1: Foundation Building (2023)
[Section titled “Phase 1: Foundation Building (2023)”](#phase-1-foundation-building-2023)
**Q: How do we validate technical feasibility and establish a foundation?**
This period was invested to ensure a technical base that would support stable development in subsequent phases.
Key milestones:
* Project kickoff and initial planning
* Core design and implementation
AI used: GPT-4
### Phase 2: Service Integration (Early 2024)
[Section titled “Phase 2: Service Integration (Early 2024)”](#phase-2-service-integration-early-2024)
**Q: How do we validate through real-world service deployment?**
The system was safely integrated into production by maintaining the existing system, applying dual writes, and gradually migrating read functionality after sufficient validation.
Key activities:
* First production service write/read migration: [KakaoTalk Gift Wish](/stories/use-cases/kakaotalk-gift-wish/)
AI used: GPT-4
### Phase 3: Stabilization (Late 2024)
[Section titled “Phase 3: Stabilization (Late 2024)”](#phase-3-stabilization-late-2024)
**Q: How do we stabilize the system and expand tenants?**
Starting from a multi-tenant architecture, this phase introduced support for independent tenant configurations to ensure service isolation and enable isolated deployments.
Key improvements:
* System stabilization and tenant expansion
* Support for independent tenant configurations to isolate services
* Operations automation and monitoring enhancement
* Applied across commerce domain (e.g., [KakaoTalk Gift Recent Views](/stories/use-cases/kakaotalk-gift-recent-views/))
AI used: Claude 3.5 Sonnet
### Phase 4: Building Sustainable Operations (Early 2025)
[Section titled “Phase 4: Building Sustainable Operations (Early 2025)”](#phase-4-building-sustainable-operations-early-2025)
**Q: How do we improve operational efficiency and address technical debt?**
During this phase, Actionbase continued to be applied to various services while improving its operational foundation. Key efforts included reducing operational overhead, improving developer tooling, and addressing accumulated technical debt.
Key activities:
* Applied across multiple domains (e.g., [KakaoTalk Friends](/stories/use-cases/kakaotalk-friends/))
AI used: GPT-4o, Claude 3.5 Sonnet
### Phase 5: Preparing for Open Source (2025)
[Section titled “Phase 5: Preparing for Open Source (2025)”](#phase-5-preparing-for-open-source-2025)
**Q: How do we prepare for open-source release while expanding adoption?**
Building on the foundation from Phase 4, this phase focused on making Actionbase ready for open source. The reimplementation process led to the decision to open-source Actionbase. While legacy code remains, the reimplementation continues to progress toward a more maintainable codebase. During this period, Actionbase was broadly adopted across [Kakao](/project/about-kakao/)’s services.
Key changes:
* Maintained API compatibility with existing systems
* Redesigned API structure
* Repository separation and organization
* Separated internal and open-source areas
AI used: GPT-4o, Claude 3.5 Sonnet, Cursor
## Open Source Release (January 2026)
[Section titled “Open Source Release (January 2026)”](#open-source-release-january-2026)
After this MVP period, Actionbase was open-sourced in January 2026.
The codebase released is the same one that was developed, operated, and refined inside Kakao. Only minimal modifications were applied to remove security-sensitive or organization-specific details, and some internal features will be opened gradually as they are reviewed.
### Why We Open-Sourced Actionbase
[Section titled “Why We Open-Sourced Actionbase”](#why-we-open-sourced-actionbase)
The same interaction features—likes, recent views, follows—were being rebuilt across teams, each time with different stacks, different schemas, different failure modes. And when traffic grew, each hit similar scaling walls.
Actionbase emerged as an attempt to stop solving the same problem ten different ways—and to solve it at scale.
**Sharing a real journey**
This codebase wasn’t designed for release. It grew through production incidents, shifting requirements, and years of real traffic. We chose to share it as-is—not a rewritten showcase, but the actual code that survived.
**Outliving its creators**
Internal systems often fade when their original maintainers move on. Open-sourcing creates a chance for the system to grow beyond any single team or company—shaped by those who actually use it.
**Becoming part of something larger**
Inside a company, an internal service is just another API to call. Developers designing new systems rarely adopt it as a core component, working with well-known technologies feels more valuable for their growth.
We understand this. By open-sourcing Actionbase, we hope it can become part of architectures we never imagined.
### Was It the Right Call?
[Section titled “Was It the Right Call?”](#was-it-the-right-call)
Actionbase runs in production at over a million requests per minute—but proving something works is not the same as proving it was the right call. What you see here is what survived: scaling walls, live incidents with users waiting, and fixes shipped while the complaints were still coming in.
We took the consolidation path, and scaling came with it. If you’ve rebuilt the same interaction features across teams, or scaled a single system until it broke, we’d like to learn from it—whether you faced the same fork, or found a different way entirely.
### Acknowledgements
[Section titled “Acknowledgements”](#acknowledgements)
This work was supported by Kakao’s leaders and engineers, who helped the team build and operate the system.
Special appreciation goes to the HBase engineers at Kakao. Actionbase’s reliability benefits from the stability provided by HBase.
This release reflects the efforts of those who contributed to the system and the teams who supported its development. We look forward to continuing this project together with the open-source community.
# Kubernetes Provisioning
> Kubernetes-based deployment guide for Actionbase
Note
This guide is being prepared.
At Kakao, Actionbase is currently provisioned using Kubernetes. This documentation will include deployment manifests, high availability configurations, performance tuning, monitoring setup, and security best practices.
* To try Actionbase, see [Quick Start](/quick-start/)
* To contribute, see [Development Setup](https://github.com/kakao/actionbase/blob/main/CONTRIBUTING.md#development-setup)
# Stories Overview
> Real-world patterns from production deployments at Kakao
Actionbase powers tens of millions of user interactions across Kakao services. Here are the patterns we’ve applied in production—and the vision we’re building toward.
## Use Cases
How Actionbase powers production—from wish lists to friend graphs.
[Gift - Recent Views (Async) ](/stories/use-cases/kakaotalk-gift-recent-views/)Async processing for high-frequency interactions
[Friends (CQRS) ](/stories/use-cases/kakaotalk-friends/)Adding Actionbase as a flexible query layer for friend relationships
[Gift - Wish (SSOT) ](/stories/use-cases/kakaotalk-gift-wish/)Migrating KakaoTalk Gift's wish list from MySQL to Actionbase
## Engineering
Inside Actionbase: how it works and how we operate it.
[Pipeline (Integration) ](/stories/engineering/pipeline/)Enabling analytics and operations through event streaming
## How We Survived
Earning trust as a database: tests as contracts, data consistency, and hard lessons learned.
[Tests as Contracts ](/stories/how-we-survived/contracts/)How Actionbase evolved continuously while preserving promised behaviors
[HBase Consistency ](/stories/how-we-survived/hbase-consistency/)Periodically verifying and correcting consistency between State, Index, and Count
[Migration Verification ](/stories/how-we-survived/migration-verification/)Verifying data integrity when migrating from Source DB to Actionbase
[Shadow Testing ](/stories/how-we-survived/shadow-testing/)Pre-deployment verification by mirroring production traffic
## Vision
Where we are heading and what we dream of building.
[Unified Graph ](/stories/vision/unified-graph/)When individual features converge into a larger structure
# Pipeline (Integration)
> Enabling analytics and operations through event streaming
> This story shares how Kakao uses Actionbase internally. We’re preparing to open source these components — see [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md).
This story demonstrates the **Integration Pipeline** pattern: how Actionbase satisfies downstream requirements without building them itself.
## The Challenge
[Section titled “The Challenge”](#the-challenge)
Actionbase is optimized for OLTP—fast reads and writes for user-facing interactions. But teams need more:
* Analytics and dashboards
* Fraud detection
* ML training data
* Operations (CS, disaster recovery)
Building these into Actionbase would compromise its core mission. But ignoring them isn’t an option either.
## The Strategy
[Section titled “The Strategy”](#the-strategy)
The answer: delegation through event streaming.
```
flowchart LR
AB[Actionbase] -->|WAL/CDC| Kafka[(Kafka)]
Kafka --> Druid[(Druid)]
Kafka --> ES[(Elasticsearch)]
Kafka --> Spark[Spark Streaming]
Spark -->|snapshot| Iceberg[(Iceberg)]
```
Every mutation in Actionbase produces WAL and CDC events. By publishing these to Kafka, Actionbase delegates downstream requirements to specialized systems—while staying focused on what it does best.
### WAL/CDC to Kafka
[Section titled “WAL/CDC to Kafka”](#walcdc-to-kafka)
Actionbase publishes events to Kafka at two points:
* WAL: Mutation request as-is—replay to rebuild state (idempotent)
* CDC: Stored result after processing—accumulate for current snapshot
See [Mutation](/design/mutation/) for the full flow.
### Analytics Backends
[Section titled “Analytics Backends”](#analytics-backends)
Different backends serve different needs:
* Spark Streaming: Complex event processing, periodic batch jobs
* Druid: Real-time aggregations, dashboards
* Elasticsearch: Real-time log search, event-by-event lookup (WAL/CDC)
* OLAP engines (Presto, Hive, etc.): Ad-hoc queries on large datasets
### Snapshots
[Section titled “Snapshots”](#snapshots)
Originally, Spark Streaming dumped Kafka periodically, and Spark Batch created snapshots. Recently migrating to Iceberg for more efficient storage.
## What This Enables
[Section titled “What This Enables”](#what-this-enables)
By publishing WAL/CDC, Actionbase doesn’t implement these—but makes them possible:
CDC consumers (current state):
* Analytics: OLAP queries, dashboards, insight extraction
* Fraud detection: Abuse patterns, anomaly detection
* ML: Training data for recommendations
* Customer support: All mutations stored in Elasticsearch for short-term CS queries
WAL consumers (replay):
* Async processing: [Recent Views](/stories/use-cases/kakaotalk-gift-recent-views/) consumes WAL and sends mutations back
* Operations: Data migration, disaster recovery, consistency checks
Actionbase focuses on OLTP. The requirements are still met—through delegation.
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* Do one thing well. Actionbase handles OLTP; specialized systems handle the rest.
* Events are the integration layer. WAL/CDC to Kafka decouples producers from consumers.
* Delegation satisfies requirements. You don’t have to build everything yourself.
This pattern lets Actionbase stay focused while enabling capabilities far beyond its core.
# How We Survived
> How we earned trust in production
Actionbase was deployed to production before it was “complete.” It had to keep evolving while maintaining production contracts.
Tests passed. But we couldn’t confidently answer “is there really no problem?” We walked along streams in Pangyo (Tancheon and Unjungcheon) discussing this concern. How could we build a state where even we could trust the system?
So we built verification layers.
## Verification Layers
[Section titled “Verification Layers”](#verification-layers)
| Stage | Method | Purpose |
| ---------- | --------------------------------------------------------------------------- | ------------------------------------------------ |
| Dev | [Tests as Contracts](/stories/how-we-survived/contracts/) | Define and protect promised behaviors |
| Pre-Deploy | [Shadow Testing](/stories/how-we-survived/shadow-testing/) | Verify new versions with production traffic |
| Migration | [Comparison Verification](/stories/how-we-survived/migration-verification/) | Confirm Source DB and Actionbase data match |
| Runtime | [HBase Consistency](/stories/how-we-survived/hbase-consistency/) | Maintain consistency between State, Index, Count |
Each layer catches different types of problems. One layer isn’t enough.
## What We Gained
[Section titled “What We Gained”](#what-we-gained)
Through verification, we gained confidence: **even if issues occur, we can detect and correct them**.
As this confidence accumulated, we could trust Actionbase even when it was the ledger itself. This wasn’t a design decision made upfront. It was trust earned by building verification.
The goal isn’t to prevent all failures. It’s to detect and fix them before they matter.
# Tests as Contracts
> How Actionbase evolved continuously while preserving promised behaviors
This story demonstrates the **Tests as Contracts** pattern: how Actionbase evolved continuously while preserving promised behaviors.
## What Is Tests as Contracts?
[Section titled “What Is Tests as Contracts?”](#what-is-tests-as-contracts)
**Test = Spec = Doc = Guard.** One source of truth.
When a service team integrates with Actionbase, we avoid manual documentation. Instead, we co-author a scenario test. That test:
* Defines the contract (schema, mutations, queries)
* Generates documentation automatically
* Runs on every PR to gate changes before production
If the test passes, the promise holds. If it fails, the change is blocked.
## Why We Needed This
[Section titled “Why We Needed This”](#why-we-needed-this)
Services rely on specific behaviors: pagination size, index filters, query direction, batch semantics, consistency guarantees. One team depends on descending timestamp order. Another expects exactly 100 items per batch. Combine these, and the number of possible usage patterns explodes.
We have unit tests. We have E2E tests. We do our best. But even with all that, we couldn’t be 100% certain that every promised usage pattern would keep working. We needed a way to evolve continuously without breaking existing integrations. Contract tests fill that gap—they guarantee exactly what we promise.
## How It Works
[Section titled “How It Works”](#how-it-works)
### 1. Integration: Tests Become Contracts
[Section titled “1. Integration: Tests Become Contracts”](#1-integration-tests-become-contracts)
```
flowchart LR
Request[Request] --> Test["Write Test"]
Test --> Draft["Draft Contract"]
Draft -->|Iterate| Test
Draft -->|Finalize| Contract
subgraph Guard [" "]
Contract["Contract"]
end
Contract --> Integrate["Integration"]
```
When a service team wants to integrate with Actionbase, the process starts with a concrete request:
“We need the 10 most recent wishes for a user, sorted by timestamp descending.”
Instead of writing documentation, we write a scenario test together. That test defines:
* The schema (edges, properties, indexes)
* The mutations (create, update, delete)
* The queries (exact access patterns, limits, ordering)
This test is not an example. It is the contract.
From that test, documentation is generated automatically. When tests run, they generate schema definitions, API examples, and query semantics as `.mdx` files—then CI deploys them to our documentation site. The service team reviews. We iterate. We adjust the test. Writing the test is manual. Everything after that is automated.
Once everyone agrees, the contract is locked—both sides must approve any changes or retirement. At that moment, the test stops being just a test. It becomes a promise.
The contracts we write cover the usage patterns that services depend on. Writing these contracts takes more work, but it’s a choice we made—for trust.
### 2. Protection: Locked Contracts Guard Production
[Section titled “2. Protection: Locked Contracts Guard Production”](#2-protection-locked-contracts-guard-production)
```
flowchart LR
Change[Code Change] --> PR[PR]
PR --> TestSuite
TestSuite -->|Pass| Merge[Merge]
TestSuite -->|Fail| Block[Blocked]
subgraph TestSuite ["Test Suite"]
UnitE2E["Unit + E2E"]
Contracts["Contracts"]
end
Feature([New Feature]) -.-> UnitE2E
Integration([New Integration]) -.-> Contracts
```
Locked contracts stay until both sides agree—whether to evolve or retire. Every pull request runs unit tests, E2E tests, and all locked contracts.
* **Unit + E2E tests** — when adding new features
* **Contract tests** — when adding new integrations
If everything passes, the change ships. If even one contract fails, the PR cannot be merged—for everything we explicitly promised. We don’t ask: “Is this change reasonable?” We ask: “Does this break anything we promised?”
## Living with Contracts
[Section titled “Living with Contracts”](#living-with-contracts)
### Same Table, Different Contracts
[Section titled “Same Table, Different Contracts”](#same-table-different-contracts)
One table can have many contracts. Service A fetches 100 items at once for batch export. Service B paginates 10 at a time for mobile UI. Service C requires strong consistency for real-time counters, while Service D tolerates eventual consistency for analytics. Each usage pattern is a separate contract, protected independently.
### Evolving Contracts
[Section titled “Evolving Contracts”](#evolving-contracts)
Contracts evolve with service requirements. Each change increments the version—old versions remain until services migrate.
## How We Differ from Traditional Contract Testing
[Section titled “How We Differ from Traditional Contract Testing”](#how-we-differ-from-traditional-contract-testing)
Traditional contract testing (like Pact) focuses on API shape—request and response formats. Our contracts go deeper: they capture usage patterns, not just interfaces.
| | Traditional | Actionbase |
| -------------- | --------------------- | ---------------------- |
| **Scope** | API shape | Usage patterns |
| **Docs** | Separate | Generated from tests |
| **Guarantees** | Payload compatibility | Behavior compatibility |
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* **Contracts are promises, not documents.** Every integration is a promise. Tests enforce that promise on every PR.
* **Evolving systems need guardrails.** Actionbase was never finished—new features, new optimizations, new storage backends. But every change was safe because every promise was tested.
* **Trust comes from guarantees, not goodwill.** Service teams stopped asking “will this break us?” They knew: if the contract passes, they’re safe.
## Appendix: Contract Test Structure
[Section titled “Appendix: Contract Test Structure”](#appendix-contract-test-structure)
The following is a simplified pseudo-code example showing how contract tests are structured. The actual implementation differs in details, but the core concept remains the same.
```kotlin
@Contract(
service = "gift",
feature = "wish",
outputDir = "services/gift/wish", // generated docs go here
)
class WishContract {
val context = Context.from(WishTable)
// inner class -> .mdx file
// test method -> section in the file
inner class Schema : SchemaSpec(context) { // -> schema.mdx
@Spec fun schema() = defineSchema() // ## Schema
@Spec fun sampleData() = createSampleData() // ## Sample Data
}
inner class Operations : OperationSpec(context) { // -> operations.mdx
@Spec fun createDatabase() = ddl.createDatabase()
@Spec fun createTable() = ddl.createTable()
}
inner class Integration : IntegrationSpec(context) { // -> integration.mdx
@Spec(title = "Insert edge") // ## Insert edge
fun insert() {
mutate(edge, INSERT)
}
@Spec(title = "Get edge") // ## Get edge
fun get() {
val result = get(source = "user-123", target = "product-456")
assertEquals(1, result.count)
}
@Spec(title = "Scan edges") // ## Scan edges
fun scan() {
val result = scan(source = "user-123", direction = OUT, limit = 10)
assertSortedByTimestampDesc(result)
}
@Spec(title = "Count edges") // ## Count edges
fun count() {
val result = count(source = "user-123", direction = OUT)
assertEquals(5, result.value)
}
}
}
```
The test framework is built on JUnit extensions. Each inner class generates an `.mdx` file, and each test method becomes a section within that file.
The contract testing framework isn’t included in the current release—it contains internal details we’re still sanitizing. We plan to open-source it incrementally, and we’re exploring a model where anyone can contribute contracts. In the meantime, we’ll share what we can through talks and presentations. See the [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md) for updates.
# HBase Consistency
> Periodically verifying and correcting consistency between State, Index, and Count
This story demonstrates the **Periodic Verification** pattern: how to detect and correct data inconsistencies that can occur during operations.
## Why We Needed This
[Section titled “Why We Needed This”](#why-we-needed-this)
Deployed. Migration verified. But data inconsistencies can still occur during operations. There’s no perfect atomicity in distributed systems.
## Data Structure
[Section titled “Data Structure”](#data-structure)
Actionbase stores three types of data in HBase:
* **State**: Source of truth - actual edge records
* **Index**: Derived data for queries
* **Count**: Aggregated data
```
flowchart LR
Mutation[Mutation] --> Batch[Batch Operation]
Batch --> State[State]
Batch --> Index[Index]
Batch --> Count[Count]
```
A single mutation updates State, Index, and Count together.
## Consistency Problem
[Section titled “Consistency Problem”](#consistency-problem)
HBase batch operations are not atomic. If a region server fails mid-operation or network issues cause partial writes, only some may update.
If State updated but Index didn’t? Queries return wrong results.
## How It Works
[Section titled “How It Works”](#how-it-works)
Verify periodically.
```
flowchart LR
HBase[(HBase)] -->|Snapshot Export| Export[Export Data]
Export --> Spark[Verification Job]
Spark --> Check1{State = Index?}
Spark --> Check2{State = Count?}
Check1 -->|Mismatch| Repair[Repair Queue]
Check2 -->|Mismatch| Repair
```
Export HBase snapshots and run verification jobs:
* **State vs Index**: Does every state have a corresponding index?
* **State vs Count**: Does the aggregation match actual record count?
## Correction
[Section titled “Correction”](#correction)
When mismatch is detected, we correct it. State is the truth. Index and Count can be regenerated from State.
Correction frequency is determined by service SLA.
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* **Don’t expect perfect atomicity.** Partial failures happen in distributed systems. Mechanisms to detect and correct them are necessary.
* **State is the truth.** Design derived data (Index, Count) so it can always be regenerated from State.
# Migration Verification
> Verifying data integrity when migrating from Source DB to Actionbase
This story demonstrates the **Comparison Verification** pattern: how to verify data integrity when migrating data from existing systems to Actionbase.
## Why We Needed This
[Section titled “Why We Needed This”](#why-we-needed-this)
Migrating data from existing systems to Actionbase. How can we be confident the data was transferred correctly?
We didn’t migrate all data at once. For [KakaoTalk Gift Wish](/stories/use-cases/kakaotalk-gift-wish/), we went through 5 stages. Verify at each stage, then proceed to the next.
## How It Works
[Section titled “How It Works”](#how-it-works)
```
flowchart LR
subgraph Source [Source DB]
SourceDump[Dump]
end
subgraph AB [Actionbase]
ABCDC[CDC] -->|Accumulate| ABSnapshot[Snapshot]
end
SourceDump --> Compare{Compare}
ABSnapshot --> Compare
Compare -->|Match| Verified[Verified]
Compare -->|Mismatch| Investigate[Investigate]
```
We compare two data sources:
1. **Source DB Dump**: Direct export from source database
2. **Actionbase CDC Snapshot**: Accumulated “after” values from Actionbase CDC
If they match, proceed to the next stage. If mismatch, stop and investigate.
## Boundary Handling
[Section titled “Boundary Handling”](#boundary-handling)
Source DB Dump and CDC Snapshot timestamps don’t align exactly. Boundary data exists.
Boundary data is excluded from the current verification window. If the verification window is `T-1` day 00:00 \~ 23:59, boundary data is verified in the next window `T`. As the window slides, all data gets verified.
## Why CDC Snapshot
[Section titled “Why CDC Snapshot”](#why-cdc-snapshot)
We could read HBase directly and compare. But CDC Snapshot is data created through an independent path.
```plaintext
Source DB → Actionbase → HBase → CDC → Snapshot
```
This entire pipeline must work correctly for them to match. If something goes wrong anywhere in the pipeline, mismatch occurs.
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* **Verify with independent sources.** Create the same data through different paths and compare. If they match, the entire pipeline is working.
* **Progress in stages.** Don’t migrate all data at once. Verify at each stage before proceeding.
# Shadow Testing
> Pre-deployment verification by mirroring production traffic
This story demonstrates the **Shadow Testing** pattern: how to perform final verification before deployment by mirroring production traffic.
## Why We Needed This
[Section titled “Why We Needed This”](#why-we-needed-this)
[Tests as Contracts](/stories/how-we-survived/contracts/) passed. But there are areas tests can’t cover:
* Bugs that only occur with specific data combinations
* Performance issues dependent on traffic patterns
* Issues that only surface at production data scale
* Clients using behaviors not specified in contracts
These are conditions you can’t create in test environments.
## How It Works
[Section titled “How It Works”](#how-it-works)
Nginx Ingress in front of production Actionbase logs all requests and responses as Access Logs. These logs go to Kafka, and the same requests are replayed to the test environment. Since it’s log-based, there’s no impact on live service traffic.
```
flowchart LR
Client[Client] --> Nginx[Nginx Ingress]
Nginx --> Prod[Production Actionbase]
Nginx -->|Access Log| Kafka[Kafka]
Kafka -->|Replay| Test[Test Actionbase]
Prod -.->|Prod Response| Compare{Match?}
Test -.->|Test Response| Compare
Compare -->|Yes| Deploy[Deploy]
Compare -->|No| Block[Blocked]
```
1. **Capture**: Nginx Ingress logs requests and responses to Kafka as Access Logs
2. **Replay**: Same requests replayed to test environment
3. **Compare**: Compare production response with test response
4. **Gate**: Block deployment on mismatch
## Deployment Process
[Section titled “Deployment Process”](#deployment-process)
```
flowchart LR
Dev[Development] --> Contract[Tests as Contracts]
Contract --> Shadow[Shadow Testing]
Shadow --> Deploy[Deploy]
```
Both stages must pass before deployment proceeds.
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* **Real traffic is irreplaceable.** No matter how sophisticated test scenarios are, they can’t perfectly reproduce production traffic patterns.
* **Final gate before deployment.** Both Tests as Contracts and Shadow Testing must pass before deployment.
# Friends (CQRS)
> Adding Actionbase as a flexible query layer for friend relationships
This story demonstrates the **CQRS (Command Query Responsibility Segregation)** pattern: how Actionbase became a read-optimized view layer for KakaoTalk’s friend relationships.
## The Challenge
[Section titled “The Challenge”](#the-challenge)
Friend relationships in KakaoTalk are stored across dozens of sharded MySQL databases. To serve read-heavy queries, an HBase-based view layer was already in place:
```
flowchart LR
App[Application] -->|write| MySQL[(MySQL Shards)]
MySQL -->|sync| HBase[(HBase View)]
App -->|read| MySQL
App -->|read| HBase
```
This worked for the original use case. But as requirements evolved, limitations emerged:
* The HBase view was designed for a specific access pattern
* Limited flexibility for new query types and schema changes
We added Actionbase—not as a replacement, but as an additional view layer.
## Integration Strategy
[Section titled “Integration Strategy”](#integration-strategy)
Rather than replacing the existing system, we added Actionbase alongside it:
```
flowchart LR
Existing[Existing System] --> Debezium[Debezium CDC]
Debezium --> Kafka[Kafka]
Kafka --> AB[(Actionbase)]
App[Application] -->|write| Existing
App -->|read| Existing
App -->|read| AB
```
The key insight: Actionbase doesn’t need to be the source of truth. It can serve as a flexible view, consuming changes via CDC. The existing system (MySQL + HBase) remained unchanged.
### Stage 1: CDC Pipeline
[Section titled “Stage 1: CDC Pipeline”](#stage-1-cdc-pipeline)
First, we set up the data flow. Debezium captured changes from MySQL shards and published them to Kafka. Actionbase consumed these events and applied mutations.
### Stage 2: Bulk Load
[Section titled “Stage 2: Bulk Load”](#stage-2-bulk-load)
For historical data, we:
1. Dumped the MySQL tables
2. Bulk-loaded into Actionbase
3. Replayed the WAL to catch up with changes during the dump
> **Note:** The migration pipeline (bulk loading) is currently internal. Open source release is in progress — see [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md).
This gave us a consistent snapshot without downtime.
### Stage 3: New Query Layer
[Section titled “Stage 3: New Query Layer”](#stage-3-new-query-layer)
With Actionbase in place, new systems used it for all query types—get, scan, count, and reverse—through a single schema definition.
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* **Actionbase works as a CQRS view.** It doesn’t have to own the data to add value.
* **Source DB CDC enables non-invasive integration.** No changes to the write path required.
* **Schema flexibility unlocks new use cases.** Get, scan, count, reverse—all from a single schema definition.
This pattern opened the door to adding Actionbase alongside existing systems without migration risk.
# Gift - Recent Views (Async)
> Async processing for high-frequency interactions
This story demonstrates the **Asynchronous Processing** pattern: how Actionbase handled high-frequency writes for KakaoTalk Gift’s recent views.
## The Challenge
[Section titled “The Challenge”](#the-challenge)
Recent views are different from other interactions. Every time a user browses a product, a view event is generated. The write volume is significantly higher than likes or wishes.
Processing these writes synchronously in the user-facing request path would:
* Increase response latency
* Create backpressure during traffic spikes
* Risk service degradation under load
Actionbase needed a pattern that could absorb high write throughput without affecting user experience.
## Async Strategy
[Section titled “Async Strategy”](#async-strategy)
Rather than processing mutations synchronously, we used async processing:
```
flowchart LR
App[Application] -->|request| AB[Actionbase]
AB -->|queue=true| WAL[(WAL)]
WAL --> Processor[Async Processor]
Processor -->|mutation| AB
App -->|read| AB
```
The key insight: separate the acknowledgment from the processing. The user gets an immediate response while the actual mutation happens in the background.
### Stage 1: Queue to WAL
[Section titled “Stage 1: Queue to WAL”](#stage-1-queue-to-wal)
First, when a view event arrives, Actionbase writes it to WAL with `queue=true` and returns immediately. No locking, no state computation—just append to the log.
### Stage 2: Processing
[Section titled “Stage 2: Processing”](#stage-2-processing)
Next, a Spark Streaming job consumes the WAL entries and sends mutations back to Actionbase. The processor:
* Batches events for efficiency
* Throttles during traffic spikes
* Retries on transient failures
> **Note:** The async processor is currently internal. Open source release is in progress — see [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md).
### Stage 3: Background Mutation
[Section titled “Stage 3: Background Mutation”](#stage-3-background-mutation)
Finally, Actionbase processes the mutations in the background—acquiring locks, updating state, computing indexes and counts. Data is typically reflected within tens of milliseconds.
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* **Async processing handles write spikes gracefully.** The WAL absorbs bursts; the processor drains at a sustainable rate.
* **Immediate response improves user experience.** Users don’t wait for the full mutation cycle.
* **Eventual consistency is acceptable for some use cases.** Recent views don’t need real-time accuracy—tens of milliseconds delay is fine.
This pattern became the template for high-frequency interactions at Kakao.
# Gift - Wish (SSOT)
> Migrating KakaoTalk Gift's wish list from MySQL to Actionbase
This story demonstrates the **SSOT (Single Source of Truth)** pattern: how KakaoTalk Gift’s wish list became Actionbase’s first production deployment.
## The Challenge
[Section titled “The Challenge”](#the-challenge)
The wish list feature in KakaoTalk Gift allowed users to save gifts they wanted to receive. The existing architecture looked like this:
```
flowchart LR
App[Application] --> MySQL[(MySQL)]
MySQL --> Batch[Spring Batch]
Batch --> Redis[(Redis)]
```
* **MySQL** stored the wish data and served forward queries (get, scan, count by user)
* **Spring Batch** aggregated reverse counts (how many users wished each product)
* **Redis** cached the reverse counts for fast reads
This worked well initially. But as traffic grew, we hit scaling walls:
* Table size grew beyond comfortable limits
* Keeping data consistent between MySQL and Redis became complex
We considered sharding MySQL. But sharding brings its own complexity—shard key management, cross-shard queries, operational overhead. Actionbase got the chance to prove itself here.
## Migration Strategy
[Section titled “Migration Strategy”](#migration-strategy)
We didn’t flip a switch. The migration happened in careful stages over several months.
### Stage 1: Dual Write
[Section titled “Stage 1: Dual Write”](#stage-1-dual-write)
```
flowchart LR
App[Application] -->|write| Existing[Existing System]
App -->|write| AB[(Actionbase)]
App -->|read| Existing
```
First, we added Actionbase as a write target alongside the existing system (MySQL + Redis). Reads still came from the existing system.
For historical data, we:
1. Dumped the MySQL table
2. Bulk-loaded into Actionbase
3. Replayed the WAL to catch up with writes that happened during the dump
> **Note:** The migration pipeline (bulk loading) is currently internal. Open source release is in progress — see [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md).
This gave us a consistent snapshot without downtime.
### Stage 2: Validation
[Section titled “Stage 2: Validation”](#stage-2-validation)
For one month, we compared:
* MySQL dumps (source of truth)
* Actionbase CDC-based snapshots
The data matched. We had confidence in consistency.
### Stage 3: Dual Read
[Section titled “Stage 3: Dual Read”](#stage-3-dual-read)
```
flowchart LR
App[Application] -->|write| Existing[Existing System]
App -->|write| AB[(Actionbase)]
App -->|read| Existing
App -.->|shadow read| AB
```
Next, we added shadow reads to Actionbase—calling it but not using the results. This validated that Actionbase could handle production traffic patterns without affecting users.
### Stage 4: Read Cutover
[Section titled “Stage 4: Read Cutover”](#stage-4-read-cutover)
```
flowchart LR
App[Application] -->|write| Existing[Existing System]
App -->|write| AB[(Actionbase)]
App -->|read| AB
```
After confirming traffic handling, we switched reads to Actionbase. At this point, Actionbase became the source of truth. The existing system remained as a backup—if anything went wrong, we could roll back instantly.
### Stage 5: Cleanup
[Section titled “Stage 5: Cleanup”](#stage-5-cleanup)
Months later, with no issues, we removed the old system entirely:
```
flowchart LR
App[Application] --> AB[(Actionbase)]
```
No more batch jobs. No more consistency issues. Just Actionbase.
## What We Learned
[Section titled “What We Learned”](#what-we-learned)
* **Gradual migration reduces risk.** Dual write, then dual read, then cutover. Each stage validates the next.
* **Keep rollback paths open.** We maintained the existing system for months after the cutover. Peace of mind matters.
* **WAL replay enables zero-downtime bulk loads.** Dump, load, replay—no data loss.
This pattern became the template for source-of-truth migrations at Kakao.
# Unified Graph
> When individual features converge into a larger structure
This is the story of where Actionbase is heading—and the problem we’re solving.
## The Pattern
[Section titled “The Pattern”](#the-pattern)
Consider the stories so far:
* **Wish** — users saving gifts they want to receive
* **Recent Views** — users browsing products
* **Friends** — users connecting with users
Each started as an isolated feature. Different teams, different tables, different scaling strategies. But from a data modeling perspective, they share the same structure:
**Who** did **what** to which **target**.
## The Convergence
[Section titled “The Convergence”](#the-convergence)
As more features adopt Actionbase, a structure emerges:
```
flowchart TD
User((User))
User -->|wishes| Product1[Product A]
User -->|views| Product2[Product B]
User -->|follows| Friend((Friend))
Friend -->|wishes| Product1
Friend -->|views| Product3[Product C]
```
Individual edges accumulate. What was once scattered across databases becomes a connected graph:
* Products connect around users
* Users connect with users
* The flow of the entire service becomes visible
## The Problem
[Section titled “The Problem”](#the-problem)
Today, each feature queries its own slice:
* “What did I wish for?”
* “What did I view recently?”
* “Who do I follow?”
But what about: **“What did my friends wish for?”**
This query spans two features — Friends and Wish. It requires traversing the graph: get my friends, then get each friend’s wishes. Currently, no system serves this efficiently at scale. This is the problem we’re solving — and has been [our vision from the beginning](https://github.com/kakao/actionbase/issues/453):
> When your data converges in Actionbase, you may discover possibilities you couldn’t see before.
“What did my friends wish for?” is one such possibility.
## Where We Are
[Section titled “Where We Are”](#where-we-are)
In 2026, we’re preparing to make it real. With each adoption, the structure grows. With each edge, the graph becomes richer — see [Roadmap](https://github.com/kakao/actionbase/blob/main/ROADMAP.md).
## Technical Notes
[Section titled “Technical Notes”](#technical-notes)
### `EdgeIndex`: Narrow Rows
[Section titled “EdgeIndex: Narrow Rows”](#edgeindex-narrow-rows)
Actionbase uses `EdgeIndex`, a narrow row structure optimized for Scan. Why not wide rows from the start? Because narrow rows scale simply — if you need more capacity, add nodes.
```plaintext
Row Key: salt | source | tableCode | direction | indexCode | indexValues | target
Qualifier: "e" (fixed)
Value: version | properties
```
Each edge is one row. Single-hop queries (“What did I wish for?”) work well — one Scan by source prefix retrieves all edges.
### `EdgeCache`: Wide Rows (Planned)
[Section titled “EdgeCache: Wide Rows (Planned)”](#edgecache-wide-rows-planned)
Why are multi-hop queries hard? With narrow rows, “What did my friends wish for?” requires: get my N friends, then N Scans to get each friend’s wishes. N RPCs don’t scale.
Wide rows solve this:
```plaintext
Row Key: salt | source | tableCode | direction | indexCode
Qualifier: indexValues | target
Value: version | properties
```
With wide rows, all edges for one source fit in a single row as separate columns. Fetching edges for N friends becomes a single MultiGet (1 RPC to storage backend, currently HBase) instead of N Scans (N RPCs to storage backend).
### Why Both?
[Section titled “Why Both?”](#why-both)
Different roles:
* `EdgeIndex` — maintains all edges. Narrow rows scale simply. Add nodes, done.
* `EdgeCache` — keeps only top N edges for multi-hop efficiency.
But wide rows can grow unbounded. A user with millions of edges creates a massive row. A **Pruner** solves this by:
* Consuming CDC to detect changes
* Keeping only top N edges per row (by index order)
* Multi-hop queries only need top N anyway
### What This Means
[Section titled “What This Means”](#what-this-means)
* **Query Layer**: Add multi-hop API with bounded traversal depth
* **Storage Layer**: Add `EdgeCache` alongside `EdgeIndex`
* **Pruner**: Background process to maintain `EdgeCache` size via CDC
* **Migration**: Bulk-load `EdgeCache` from existing data
Both structures will coexist — `EdgeIndex` as the complete index, `EdgeCache` for multi-hop efficiency.
# REST API
> Actionbase REST API reference
Actionbase provides REST APIs for metadata management, mutations, and queries.
## Base URL
[Section titled “Base URL”](#base-url)
* Local: `http://localhost:8080`
* Deployed: your server URL (e.g., `http://ab.example.com`)
## Authentication
[Section titled “Authentication”](#authentication)
Internally, Actionbase uses a custom authentication system. We abstracted it via `CustomTokenFilter` interface, but weren’t confident the implementation was ready for public release due to security considerations. Given the time available, we focused on documentation and developer experience, leaving the interface without a default implementation. The `Authorization` header is accepted but not validated—any value works, or omit it entirely.
As we continue documenting production operations, we plan to provide authentication support as well.
## APIs
[Section titled “APIs”](#apis)
* [Metadata API](/api-references/metadata/) — Manage databases, tables, and aliases (v3)
* [Mutation API](/api-references/mutation/) — Create, update, and delete edges (v3)
* [Query API](/api-references/query/) — Query edges by count, scan, or get (v3)
# Metadata API Reference
> Metadata management API reference
Caution
Authentication is not enforced. See [API References](/api-references/#authentication).
Metadata management API for databases, tables, and aliases.
## 1. List Databases
[Section titled “1. List Databases”](#1-list-databases)
Retrieve all databases.
### Endpoint
[Section titled “Endpoint”](#endpoint)
```plaintext
GET /graph/v3/databases
```
### Parameters
[Section titled “Parameters”](#parameters)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
### Response Type
[Section titled “Response Type”](#response-type)
`List<`[DatabaseDescriptor](#databasedescriptor)`>` - List of databases
### Request Example
[Section titled “Request Example”](#request-example)
```bash
curl -X GET \
"http://ab.example.com/graph/v3/databases" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example)
```json
[
{
"tenant": "default",
"database": "mydb",
"active": true,
"comment": "My database",
"revision": 1,
"createdAt": 1700000000000,
"createdBy": "admin",
"updatedAt": 1700000000000,
"updatedBy": "admin"
}
]
```
## 2. Get Database
[Section titled “2. Get Database”](#2-get-database)
Retrieve a single database by name.
### Endpoint
[Section titled “Endpoint”](#endpoint-1)
```plaintext
GET /graph/v3/databases/{database}
```
### Parameters
[Section titled “Parameters”](#parameters-1)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
### Response Type
[Section titled “Response Type”](#response-type-1)
[DatabaseDescriptor](#databasedescriptor) - Database information
Returns 404 if not found.
### Request Example
[Section titled “Request Example”](#request-example-1)
```bash
curl -X GET \
"http://ab.example.com/graph/v3/databases/mydb" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-1)
```json
{
"tenant": "default",
"database": "mydb",
"active": true,
"comment": "My database",
"revision": 1,
"createdAt": 1700000000000,
"createdBy": "admin",
"updatedAt": 1700000000000,
"updatedBy": "admin"
}
```
## 3. Create Database
[Section titled “3. Create Database”](#3-create-database)
Create a new database.
### Endpoint
[Section titled “Endpoint”](#endpoint-2)
```plaintext
POST /graph/v3/databases
```
### Parameters
[Section titled “Parameters”](#parameters-2)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Body | database | Required | Database name |
| Body | comment | Required | Database description |
### Request Body
[Section titled “Request Body”](#request-body)
[DatabaseCreateRequest](#databasecreaterequest) - Database creation payload
### Response Type
[Section titled “Response Type”](#response-type-2)
[DatabaseDescriptor](#databasedescriptor) - Created database information
### Request Example
[Section titled “Request Example”](#request-example-2)
```bash
curl -X POST \
"http://ab.example.com/graph/v3/databases" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"database": "mydb",
"comment": "My database"
}'
```
### Response Example
[Section titled “Response Example”](#response-example-2)
```json
{
"tenant": "default",
"database": "mydb",
"active": true,
"comment": "My database",
"revision": 1,
"createdAt": 1700000000000,
"createdBy": "admin",
"updatedAt": 1700000000000,
"updatedBy": "admin"
}
```
## 4. Update Database
[Section titled “4. Update Database”](#4-update-database)
Update an existing database.
### Endpoint
[Section titled “Endpoint”](#endpoint-3)
```plaintext
PUT /graph/v3/databases/{database}
```
### Parameters
[Section titled “Parameters”](#parameters-3)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Body | active | Optional | Whether the database is active |
| Body | comment | Optional | Database description |
### Request Body
[Section titled “Request Body”](#request-body-1)
[DatabaseUpdateRequest](#databaseupdaterequest) - Database update payload
### Response Type
[Section titled “Response Type”](#response-type-3)
[DatabaseDescriptor](#databasedescriptor) - Updated database information
### Request Example
[Section titled “Request Example”](#request-example-3)
```bash
curl -X PUT \
"http://ab.example.com/graph/v3/databases/mydb" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"active": true,
"comment": "Updated description"
}'
```
## 5. Delete Database
[Section titled “5. Delete Database”](#5-delete-database)
Delete an existing database.
### Endpoint
[Section titled “Endpoint”](#endpoint-4)
```plaintext
DELETE /graph/v3/databases/{database}
```
### Parameters
[Section titled “Parameters”](#parameters-4)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
### Response Type
[Section titled “Response Type”](#response-type-4)
204 No Content (no response body)
### Request Example
[Section titled “Request Example”](#request-example-4)
```bash
curl -X DELETE \
"http://ab.example.com/graph/v3/databases/mydb" \
-H "Authorization: YOUR_API_KEY"
```
## 6. List Tables
[Section titled “6. List Tables”](#6-list-tables)
Retrieve all tables in a database.
### Endpoint
[Section titled “Endpoint”](#endpoint-5)
```plaintext
GET /graph/v3/databases/{database}/tables
```
### Parameters
[Section titled “Parameters”](#parameters-5)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
### Response Type
[Section titled “Response Type”](#response-type-5)
`List<`[TableDescriptor](#tabledescriptor)`>` - List of tables
### Request Example
[Section titled “Request Example”](#request-example-5)
```bash
curl -X GET \
"http://ab.example.com/graph/v3/databases/mydb/tables" \
-H "Authorization: YOUR_API_KEY"
```
## 7. Get Table
[Section titled “7. Get Table”](#7-get-table)
Retrieve a single table by name.
### Endpoint
[Section titled “Endpoint”](#endpoint-6)
```plaintext
GET /graph/v3/databases/{database}/tables/{table}
```
### Parameters
[Section titled “Parameters”](#parameters-6)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Path | table | Required | Table name |
### Response Type
[Section titled “Response Type”](#response-type-6)
[TableDescriptor](#tabledescriptor) - Table information
Returns 404 if not found.
### Request Example
[Section titled “Request Example”](#request-example-6)
```bash
curl -X GET \
"http://ab.example.com/graph/v3/databases/mydb/tables/follows" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-3)
```json
{
"type": "edge",
"tenant": "default",
"database": "mydb",
"table": "follows",
"schema": {
"type": "edge",
"source": { "type": "string", "comment": "user id" },
"target": { "type": "string", "comment": "user id" },
"properties": [],
"direction": "OUT",
"indexes": [],
"groups": []
},
"storage": "datastore://hbase/follows_table",
"mode": "SYNC",
"active": true,
"comment": "User follows relationship",
"revision": 1,
"createdAt": 1700000000000,
"createdBy": "admin",
"updatedAt": 1700000000000,
"updatedBy": "admin"
}
```
## 8. Create Table
[Section titled “8. Create Table”](#8-create-table)
Create a new table.
### Endpoint
[Section titled “Endpoint”](#endpoint-7)
```plaintext
POST /graph/v3/databases/{database}/tables
```
### Parameters
[Section titled “Parameters”](#parameters-7)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | ----------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Body | table | Required | Table name |
| Body | schema | Required | Edge schema definition |
| Body | storage | Required | Storage URI (`datastore:///
`) |
| Body | mode | Optional | Mutation mode (default: SYNC) |
| Body | comment | Required | Table description |
### Request Body
[Section titled “Request Body”](#request-body-2)
[TableCreateRequest](#tablecreaterequest) - Table creation payload
### Response Type
[Section titled “Response Type”](#response-type-7)
[TableDescriptor](#tabledescriptor) - Created table information
### Request Example
[Section titled “Request Example”](#request-example-7)
```bash
curl -X POST \
"http://ab.example.com/graph/v3/databases/mydb/tables" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"table": "follows",
"schema": {
"type": "EDGE",
"source": { "type": "string", "comment": "user id" },
"target": { "type": "string", "comment": "user id" },
"properties": [],
"direction": "OUT",
"indexes": [],
"groups": []
},
"storage": "datastore://hbase/follows_table",
"mode": "SYNC",
"comment": "User follows relationship"
}'
```
## 9. Update Table
[Section titled “9. Update Table”](#9-update-table)
Update an existing table.
### Endpoint
[Section titled “Endpoint”](#endpoint-8)
```plaintext
PUT /graph/v3/databases/{database}/tables/{table}
```
### Parameters
[Section titled “Parameters”](#parameters-8)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Path | table | Required | Table name |
| Body | active | Optional | Whether the table is active |
| Body | schema | Optional | Edge schema definition |
| Body | mode | Optional | Mutation mode |
| Body | comment | Optional | Table description |
### Request Body
[Section titled “Request Body”](#request-body-3)
[TableUpdateRequest](#tableupdaterequest) - Table update payload
### Response Type
[Section titled “Response Type”](#response-type-8)
[TableDescriptor](#tabledescriptor) - Updated table information
### Request Example
[Section titled “Request Example”](#request-example-8)
```bash
curl -X PUT \
"http://ab.example.com/graph/v3/databases/mydb/tables/follows" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"active": true,
"comment": "Updated description"
}'
```
## 10. Delete Table
[Section titled “10. Delete Table”](#10-delete-table)
Delete an existing table.
### Endpoint
[Section titled “Endpoint”](#endpoint-9)
```plaintext
DELETE /graph/v3/databases/{database}/tables/{table}
```
### Parameters
[Section titled “Parameters”](#parameters-9)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Path | table | Required | Table name |
### Response Type
[Section titled “Response Type”](#response-type-9)
204 No Content (no response body)
### Request Example
[Section titled “Request Example”](#request-example-9)
```bash
curl -X DELETE \
"http://ab.example.com/graph/v3/databases/mydb/tables/follows" \
-H "Authorization: YOUR_API_KEY"
```
## 11. List Aliases
[Section titled “11. List Aliases”](#11-list-aliases)
Retrieve all aliases in a database.
### Endpoint
[Section titled “Endpoint”](#endpoint-10)
```plaintext
GET /graph/v3/databases/{database}/aliases
```
### Parameters
[Section titled “Parameters”](#parameters-10)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
### Response Type
[Section titled “Response Type”](#response-type-10)
`List<`[AliasDescriptor](#aliasdescriptor)`>` - List of aliases
### Request Example
[Section titled “Request Example”](#request-example-10)
```bash
curl -X GET \
"http://ab.example.com/graph/v3/databases/mydb/aliases" \
-H "Authorization: YOUR_API_KEY"
```
## 12. Get Alias
[Section titled “12. Get Alias”](#12-get-alias)
Retrieve a single alias by name.
### Endpoint
[Section titled “Endpoint”](#endpoint-11)
```plaintext
GET /graph/v3/databases/{database}/aliases/{alias}
```
### Parameters
[Section titled “Parameters”](#parameters-11)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Path | alias | Required | Alias name |
### Response Type
[Section titled “Response Type”](#response-type-11)
[AliasDescriptor](#aliasdescriptor) - Alias information
Returns 404 if not found.
### Request Example
[Section titled “Request Example”](#request-example-11)
```bash
curl -X GET \
"http://ab.example.com/graph/v3/databases/mydb/aliases/friends" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-4)
```json
{
"tenant": "default",
"database": "mydb",
"alias": "friends",
"table": "follows",
"active": true,
"comment": "Alias for follows",
"revision": 1,
"createdAt": 1700000000000,
"createdBy": "admin",
"updatedAt": 1700000000000,
"updatedBy": "admin"
}
```
## 13. Create Alias
[Section titled “13. Create Alias”](#13-create-alias)
Create a new alias.
### Endpoint
[Section titled “Endpoint”](#endpoint-12)
```plaintext
POST /graph/v3/databases/{database}/aliases
```
### Parameters
[Section titled “Parameters”](#parameters-12)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Body | alias | Required | Alias name |
| Body | table | Required | Target table name (same database only) |
| Body | comment | Required | Alias description |
### Request Body
[Section titled “Request Body”](#request-body-4)
[AliasCreateRequest](#aliascreaterequest) - Alias creation payload
### Response Type
[Section titled “Response Type”](#response-type-12)
[AliasDescriptor](#aliasdescriptor) - Created alias information
### Request Example
[Section titled “Request Example”](#request-example-12)
```bash
curl -X POST \
"http://ab.example.com/graph/v3/databases/mydb/aliases" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"alias": "friends",
"table": "follows",
"comment": "Alias for follows"
}'
```
## 14. Update Alias
[Section titled “14. Update Alias”](#14-update-alias)
Update an existing alias.
### Endpoint
[Section titled “Endpoint”](#endpoint-13)
```plaintext
PUT /graph/v3/databases/{database}/aliases/{alias}
```
### Parameters
[Section titled “Parameters”](#parameters-13)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Path | alias | Required | Alias name |
| Body | active | Optional | Whether the alias is active |
| Body | table | Optional | Target table name |
| Body | comment | Optional | Alias description |
### Request Body
[Section titled “Request Body”](#request-body-5)
[AliasUpdateRequest](#aliasupdaterequest) - Alias update payload
### Response Type
[Section titled “Response Type”](#response-type-13)
[AliasDescriptor](#aliasdescriptor) - Updated alias information
### Request Example
[Section titled “Request Example”](#request-example-13)
```bash
curl -X PUT \
"http://ab.example.com/graph/v3/databases/mydb/aliases/friends" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"active": false,
"comment": "Deactivated alias"
}'
```
## 15. Delete Alias
[Section titled “15. Delete Alias”](#15-delete-alias)
Delete an existing alias.
Note
Alias must be deactivated (`active: false`) before deletion.
### Endpoint
[Section titled “Endpoint”](#endpoint-14)
```plaintext
DELETE /graph/v3/databases/{database}/aliases/{alias}
```
### Parameters
[Section titled “Parameters”](#parameters-14)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Database name |
| Path | alias | Required | Alias name |
### Response Type
[Section titled “Response Type”](#response-type-14)
204 No Content (no response body)
### Request Example
[Section titled “Request Example”](#request-example-14)
```bash
curl -X DELETE \
"http://ab.example.com/graph/v3/databases/mydb/aliases/friends" \
-H "Authorization: YOUR_API_KEY"
```
## Data Model
[Section titled “Data Model”](#data-model)
### DatabaseDescriptor
[Section titled “DatabaseDescriptor”](#databasedescriptor)
Database information payload.
```kotlin
data class DatabaseDescriptor(
val tenant: String, // Tenant name
val database: String, // Database name
val active: Boolean, // Whether the database is active
val comment: String, // Database description
val revision: Long, // Revision number
val createdAt: Long, // Creation timestamp
val createdBy: String, // Creator
val updatedAt: Long, // Last update timestamp
val updatedBy: String, // Last updater
)
```
### TableDescriptor
[Section titled “TableDescriptor”](#tabledescriptor)
Table information payload. Two types are supported: `edge` and `multiEdge`.
#### TableDescriptor.Edge
[Section titled “TableDescriptor.Edge”](#tabledescriptoredge)
```kotlin
data class TableDescriptor.Edge(
val type: String = "edge", // Table type discriminator
val tenant: String, // Tenant name
val database: String, // Database name
val table: String, // Table name
val schema: ModelSchema.Edge, // Edge schema definition
val storage: String, // Storage URI (`datastore:///
`)
val mode: MutationMode, // Mutation mode (SYNC, ASYNC, DROP)
val active: Boolean, // Whether the table is active
val comment: String, // Table description
val revision: Long, // Revision number
val createdAt: Long, // Creation timestamp
val createdBy: String, // Creator
val updatedAt: Long, // Last update timestamp
val updatedBy: String, // Last updater
)
```
#### TableDescriptor.MultiEdge
[Section titled “TableDescriptor.MultiEdge”](#tabledescriptormultiedge)
```kotlin
data class TableDescriptor.MultiEdge(
val type: String = "multiEdge", // Table type discriminator
val tenant: String, // Tenant name
val database: String, // Database name
val table: String, // Table name
val schema: ModelSchema.MultiEdge, // MultiEdge schema definition
val storage: String, // Storage URI (`datastore:///
`)
val mode: MutationMode, // Mutation mode (SYNC, ASYNC, DROP)
val active: Boolean, // Whether the table is active
val comment: String, // Table description
val revision: Long, // Revision number
val createdAt: Long, // Creation timestamp
val createdBy: String, // Creator
val updatedAt: Long, // Last update timestamp
val updatedBy: String, // Last updater
)
```
### AliasDescriptor
[Section titled “AliasDescriptor”](#aliasdescriptor)
Alias information payload.
```kotlin
data class AliasDescriptor(
val tenant: String, // Tenant name
val database: String, // Database name
val alias: String, // Alias name
val table: String, // Target table name
val active: Boolean, // Whether the alias is active
val comment: String, // Alias description
val revision: Long, // Revision number
val createdAt: Long, // Creation timestamp
val createdBy: String, // Creator
val updatedAt: Long, // Last update timestamp
val updatedBy: String, // Last updater
)
```
### ModelSchema.Edge
[Section titled “ModelSchema.Edge”](#modelschemaedge)
Edge schema definition. The `type` discriminator is `"EDGE"` in requests and `"edge"` in responses.
```kotlin
data class ModelSchema.Edge(
val type: String, // "EDGE" (request) / "edge" (response)
val source: Field, // Source vertex field
val target: Field, // Target vertex field
val properties: List = emptyList(),// Edge properties
val direction: DirectionType, // Direction type (OUT, IN, BOTH)
val indexes: List = emptyList(), // Index definitions
val groups: List = emptyList(), // Group definitions
val caches: List = emptyList(), // Cache definitions
)
```
### ModelSchema.MultiEdge
[Section titled “ModelSchema.MultiEdge”](#modelschemamultiedge)
MultiEdge schema definition. Supports multiple edges between the same source-target pair, distinguished by an `id` field. The `type` discriminator is `"MULTI_EDGE"` in requests and `"multiEdge"` in responses.
```kotlin
data class ModelSchema.MultiEdge(
val type: String, // "MULTI_EDGE" (request) / "multiEdge" (response)
val id: Field, // Edge identifier field (distinguishes multiple edges)
val source: Field, // Source vertex field
val target: Field, // Target vertex field
val properties: List = emptyList(),// Edge properties
val direction: DirectionType, // Direction type (OUT, IN, BOTH)
val indexes: List = emptyList(), // Index definitions
val groups: List = emptyList(), // Group definitions
val caches: List = emptyList(), // Cache definitions
)
```
### Field
[Section titled “Field”](#field)
Vertex field definition. The `type` value is lowercase in JSON (e.g., `"string"`, `"long"`).
```kotlin
data class Field(
val type: PrimitiveType, // Data type (JSON: lowercase, e.g., "string", "long")
val comment: String, // Field description
)
```
### StructField
[Section titled “StructField”](#structfield)
Property field definition. The `type` value is lowercase in JSON (e.g., `"int"`, `"long"`).
```kotlin
data class StructField(
val name: String, // Field name
val type: PrimitiveType, // Data type (JSON: lowercase, e.g., "int", "long")
val comment: String, // Field description
val nullable: Boolean, // Whether the field is nullable
)
```
### Index
[Section titled “Index”](#index)
Index definition.
```kotlin
data class Index(
val index: String, // Index name
val fields: List, // Indexed fields with sort order
val comment: String = "", // Index description
)
```
### IndexField
[Section titled “IndexField”](#indexfield)
Index field definition.
```kotlin
data class IndexField(
val field: String, // Field name
val order: Order, // Sort order (ASC, DESC)
)
```
### Group
[Section titled “Group”](#group)
Group definition for aggregation queries.
```kotlin
data class Group(
val group: String, // Group name
val type: GroupType, // Aggregation type (SUM, COUNT)
val fields: List, // Group key fields
val valueField: String = "-", // Value field name
val comment: String = "", // Group description
val directionType: DirectionType = BOTH, // Direction type
val ttl: Long = -1, // Time-to-live in milliseconds (-1 = no expiry)
)
```
### Cache
[Section titled “Cache”](#cache)
Cache definition. Precomputes a bounded, sorted projection of edges (e.g., top-N by score) for low-latency reads.
```kotlin
data class Cache(
val cache: String, // Cache name
val fields: List, // Sort keys with order
val limit: Int = 100, // Maximum number of pre-sorted entries kept per source key (must be > 0)
val comment: String = Constants.DEFAULT_COMMENT,// Cache description
)
```
> **Updating `indexes` / `groups` / `caches` via PUT**: these fields live inside `ModelSchema`, so a `PUT /graph/v3/databases/{database}/tables/{table}` body that omits `schema` does **not** change any of them. To change a cache, send the full `schema` payload with the desired `caches` array. This matches the existing semantics for `indexes` and `groups`.
### DatabaseCreateRequest
[Section titled “DatabaseCreateRequest”](#databasecreaterequest)
Database creation request payload.
```kotlin
data class DatabaseCreateRequest(
val database: String, // Database name
val comment: String, // Database description
)
```
### DatabaseUpdateRequest
[Section titled “DatabaseUpdateRequest”](#databaseupdaterequest)
Database update request payload.
```kotlin
data class DatabaseUpdateRequest(
val active: Boolean?, // Whether the database is active (optional)
val comment: String?, // Database description (optional)
)
```
### TableCreateRequest
[Section titled “TableCreateRequest”](#tablecreaterequest)
Table creation request payload.
```kotlin
data class TableCreateRequest(
val table: String, // Table name
val schema: ModelSchema, // Schema definition (ModelSchema.Edge or ModelSchema.MultiEdge)
val storage: String, // Storage URI (`datastore:///
`)
val mode: MutationMode, // Mutation mode (default: SYNC)
val comment: String, // Table description
)
```
### TableUpdateRequest
[Section titled “TableUpdateRequest”](#tableupdaterequest)
Table update request payload.
```kotlin
data class TableUpdateRequest(
val active: Boolean?, // Whether the table is active (optional)
val schema: ModelSchema?, // Schema definition (optional, ModelSchema.Edge or ModelSchema.MultiEdge)
val mode: MutationMode?, // Mutation mode (optional)
val comment: String?, // Table description (optional)
)
```
### AliasCreateRequest
[Section titled “AliasCreateRequest”](#aliascreaterequest)
Alias creation request payload.
```kotlin
data class AliasCreateRequest(
val alias: String, // Alias name
val table: String, // Target table name (same database only)
val comment: String, // Alias description
)
```
### AliasUpdateRequest
[Section titled “AliasUpdateRequest”](#aliasupdaterequest)
Alias update request payload.
```kotlin
data class AliasUpdateRequest(
val active: Boolean?, // Whether the alias is active (optional)
val table: String?, // Target table name (optional)
val comment: String?, // Alias description (optional)
)
```
### Enums
[Section titled “Enums”](#enums)
```kotlin
enum class MutationMode { SYNC, ASYNC, DROP, DENY }
enum class DirectionType { BOTH, OUT, IN }
enum class PrimitiveType { BOOLEAN, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, STRING, OBJECT }
enum class Order { ASC, DESC }
enum class GroupType { SUM, COUNT }
```
Note
`PrimitiveType` values are serialized as **lowercase** in JSON (e.g., `"string"`, `"int"`, `"long"`). All other enums use uppercase (e.g., `"SYNC"`, `"OUT"`).
### Validation Rules
[Section titled “Validation Rules”](#validation-rules)
| Field | Rule |
| ----------------------------- | -------------------------------------------------------------------- |
| Database / Table / Alias name | `^[a-zA-Z][a-zA-Z0-9_-]{0,63}$` — starts with a letter, max 64 chars |
| Storage URI | `^datastore://[a-z_]+/[a-zA-Z0-9_]+$` |
| Comment | Max 1000 characters |
## V2 Compatibility
[Section titled “V2 Compatibility”](#v2-compatibility)
V3 API wraps the existing V2 DDL services with a new interface. The table below shows the mapping between V2 and V3 terminology.
### Terminology Mapping
[Section titled “Terminology Mapping”](#terminology-mapping)
| V2 Term | V3 Term | Description |
| ------- | -------- | ----------------------- |
| Service | Database | Logical database |
| Label | Table | Table with edge schema |
| Alias | Alias | Table alias (unchanged) |
### Label Type Mapping
[Section titled “Label Type Mapping”](#label-type-mapping)
| V2 LabelType | V3 Table Type | Conversion |
| ------------ | ------------- | ----------------------------------- |
| INDEXED | edge | Bidirectional |
| MULTI\_EDGE | multiEdge | Bidirectional |
| HASH | edge | One-way (V2 to V3 only, deprecated) |
### API Endpoint Mapping
[Section titled “API Endpoint Mapping”](#api-endpoint-mapping)
| V2 Endpoint | V3 Endpoint |
| ----------------------------------------------------- | ------------------------------------------------------- |
| `GET /graph/v2/services` | `GET /graph/v3/databases` |
| `GET /graph/v2/services/{service}` | `GET /graph/v3/databases/{database}` |
| `POST /graph/v2/services/{service}` | `POST /graph/v3/databases` |
| `PUT /graph/v2/services/{service}` | `PUT /graph/v3/databases/{database}` |
| `DELETE /graph/v2/services/{service}` | `DELETE /graph/v3/databases/{database}` |
| `GET /graph/v2/services/{service}/labels` | `GET /graph/v3/databases/{database}/tables` |
| `GET /graph/v2/services/{service}/labels/{label}` | `GET /graph/v3/databases/{database}/tables/{table}` |
| `POST /graph/v2/services/{service}/labels/{label}` | `POST /graph/v3/databases/{database}/tables` |
| `PUT /graph/v2/services/{service}/labels/{label}` | `PUT /graph/v3/databases/{database}/tables/{table}` |
| `DELETE /graph/v2/services/{service}/labels/{label}` | `DELETE /graph/v3/databases/{database}/tables/{table}` |
| `GET /graph/v2/services/{service}/aliases` | `GET /graph/v3/databases/{database}/aliases` |
| `GET /graph/v2/services/{service}/aliases/{alias}` | `GET /graph/v3/databases/{database}/aliases/{alias}` |
| `POST /graph/v2/services/{service}/aliases/{alias}` | `POST /graph/v3/databases/{database}/aliases` |
| `PUT /graph/v2/services/{service}/aliases/{alias}` | `PUT /graph/v3/databases/{database}/aliases/{alias}` |
| `DELETE /graph/v2/services/{service}/aliases/{alias}` | `DELETE /graph/v3/databases/{database}/aliases/{alias}` |
### MutationMode Mapping
[Section titled “MutationMode Mapping”](#mutationmode-mapping)
| V2 Mode | V3 Mode | Description |
| ------- | ------- | -------------------------------------- |
| SYNC | SYNC | Synchronous mutation |
| ASYNC | ASYNC | Asynchronous mutation |
| IGNORE | DROP | Drop mutations silently |
| - | DENY | Deny mutations (V3 only, throws error) |
### V3 Alias Constraints
[Section titled “V3 Alias Constraints”](#v3-alias-constraints)
V3 aliases can only reference tables within the same database. Cross-database aliases (supported in V2 via `database.table` format) are not available in V3.
# Mutation API Reference
> Edge mutation API reference
Caution
Authentication is not enforced. See [API References](/api-references/#authentication).
Edge mutation API. See [Mutation](/design/mutation/) for conceptual background.
## 1. Edge Mutation
[Section titled “1. Edge Mutation”](#1-edge-mutation)
Mutate edges between source nodes and target nodes.
### Endpoint
[Section titled “Endpoint”](#endpoint)
```plaintext
POST /graph/v3/databases/{database}/tables/{table}/edges
```
### Parameters
[Section titled “Parameters”](#parameters)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------ | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name |
| Query | lock | Optional (default: true) | Whether to acquire lock during mutation |
| Body | mutations | Required | List of mutation items |
### Request Body
[Section titled “Request Body”](#request-body)
[EdgeBulkMutationRequest](#edgebulkmutationrequest) - Payload containing mutation items
### Response Type
[Section titled “Response Type”](#response-type)
[EdgeMutationResponse](#edgemutationresponse) - Payload containing mutation results
### Request Example
[Section titled “Request Example”](#request-example)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| lock | true |
```bash
# POST /graph/v3/databases/your_database/tables/your_table/edges
curl -X POST \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges?lock=true" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mutations": [
{
"type": "INSERT",
"edge": {
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
}
}
}
]
}'
```
### Response Example
[Section titled “Response Example”](#response-example)
```json
{
"results": [
{
"source": "source1",
"target": "target1",
"status": "CREATED",
"count": 1
}
]
}
```
## 2. Edge Mutation (Sync)
[Section titled “2. Edge Mutation (Sync)”](#2-edge-mutation-sync)
Mutate edges synchronously. This endpoint waits for the mutation to complete before returning a response.
### Endpoint
[Section titled “Endpoint”](#endpoint-1)
```plaintext
POST /graph/v3/databases/{database}/tables/{table}/edges/sync
```
### Parameters
[Section titled “Parameters”](#parameters-1)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------ | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name |
| Query | lock | Optional (default: true) | Whether to acquire lock during mutation |
| Body | mutations | Required | List of mutation items |
### Request Body
[Section titled “Request Body”](#request-body-1)
[EdgeBulkMutationRequest](#edgebulkmutationrequest) - Payload containing mutation items
### Response Type
[Section titled “Response Type”](#response-type-1)
[EdgeMutationResponse](#edgemutationresponse) - Payload containing mutation results
### Request Example
[Section titled “Request Example”](#request-example-1)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| lock | true |
```bash
# POST /graph/v3/databases/your_database/tables/your_table/edges/sync
curl -X POST \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/sync?lock=true" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mutations": [
{
"type": "UPDATE",
"edge": {
"version": 2,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.9,
"type": "FOLLOWS"
}
}
}
]
}'
```
### Response Example
[Section titled “Response Example”](#response-example-1)
```json
{
"results": [
{
"source": "source1",
"target": "target1",
"status": "UPDATED",
"count": 1
}
]
}
```
## 3. Multi-Edge Mutation
[Section titled “3. Multi-Edge Mutation”](#3-multi-edge-mutation)
Mutate multi-edges identified by edge IDs.
### Endpoint
[Section titled “Endpoint”](#endpoint-2)
```plaintext
POST /graph/v3/databases/{database}/tables/{table}/multi-edges
```
### Parameters
[Section titled “Parameters”](#parameters-2)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------ | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name |
| Query | lock | Optional (default: true) | Whether to acquire lock during mutation |
| Body | mutations | Required | List of mutation items |
### Request Body
[Section titled “Request Body”](#request-body-2)
[MultiEdgeBulkMutationRequest](#multiedgebulkmutationrequest) - Payload containing mutation items
### Response Type
[Section titled “Response Type”](#response-type-2)
[MultiEdgeMutationResponse](#multiedgemutationresponse) - Payload containing mutation results
### Request Example
[Section titled “Request Example”](#request-example-2)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| lock | true |
```bash
# POST /graph/v3/databases/your_database/tables/your_table/multi-edges
curl -X POST \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/multi-edges?lock=true" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mutations": [
{
"type": "INSERT",
"edge": {
"version": 1,
"id": "edge1",
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
}
}
}
]
}'
```
### Response Example
[Section titled “Response Example”](#response-example-2)
```json
{
"results": [
{
"id": "edge1",
"status": "CREATED",
"count": 1
}
]
}
```
## 4. Multi-Edge Mutation (Sync)
[Section titled “4. Multi-Edge Mutation (Sync)”](#4-multi-edge-mutation-sync)
Mutate multi-edges synchronously. This endpoint waits for the mutation to complete before returning a response.
### Endpoint
[Section titled “Endpoint”](#endpoint-3)
```plaintext
POST /graph/v3/databases/{database}/tables/{table}/multi-edges/sync
```
### Parameters
[Section titled “Parameters”](#parameters-3)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------ | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name |
| Query | lock | Optional (default: true) | Whether to acquire lock during mutation |
| Body | mutations | Required | List of mutation items |
### Request Body
[Section titled “Request Body”](#request-body-3)
[MultiEdgeBulkMutationRequest](#multiedgebulkmutationrequest) - Payload containing mutation items
### Response Type
[Section titled “Response Type”](#response-type-3)
[MultiEdgeMutationResponse](#multiedgemutationresponse) - Payload containing mutation results
### Request Example
[Section titled “Request Example”](#request-example-3)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| lock | true |
```bash
# POST /graph/v3/databases/your_database/tables/your_table/multi-edges/sync
curl -X POST \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/multi-edges/sync?lock=true" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mutations": [
{
"type": "DELETE",
"edge": {
"version": 3,
"id": "edge1",
"source": "source1",
"target": "target1",
"properties": {}
}
}
]
}'
```
### Response Example
[Section titled “Response Example”](#response-example-3)
```json
{
"results": [
{
"id": "edge1",
"status": "DELETED",
"count": 1
}
]
}
```
## 5. Scan-Delete (Immutable Edge Tables)
[Section titled “5. Scan-Delete (Immutable Edge Tables)”](#5-scan-delete-immutable-edge-tables)
Scan an index range on an [immutable edge table](/design/schema/#immutable-edge-tables-type-immutable_indexed-v3-immutable_edge) and delete the matched rows, returning the count deleted. Used for eviction and retention.
Rejected with `400` on non-immutable tables. `limit` is required and capped at 1000; one call deletes at most one page, so loop until the returned count is below `limit` to drain a larger range.
### Endpoint
[Section titled “Endpoint”](#endpoint-4)
```plaintext
DELETE /graph/v3/databases/{database}/tables/{table}/edges/scan/{index}
```
### Parameters
[Section titled “Parameters”](#parameters-4)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | --------------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name (must be an immutable edge table) |
| Path | index | Required | Index to scan |
| Query | start | Required | Source node to scan from |
| Query | direction | Required | Scan direction (OUT, IN) |
| Query | limit | Required | Max rows to delete in this call (maximum 1000) |
| Query | ranges | Optional | Index range predicate (e.g., `seq:lte:1001`) |
### Response Type
[Section titled “Response Type”](#response-type-4)
[EdgeScanDeleteResponse](#edgescandeleteresponse) - The count deleted
### Request Example
[Section titled “Request Example”](#request-example-4)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| index | seq\_asc |
```bash
# DELETE /graph/v3/databases/your_database/tables/your_table/edges/scan/seq_asc
curl -X DELETE \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/scan/seq_asc?start=1&direction=OUT&limit=100&ranges=seq:lte:1001" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-4)
```json
{
"database": "your_database",
"table": "your_table",
"index": "seq_asc",
"deleted": 2
}
```
## Event Types
[Section titled “Event Types”](#event-types)
The `type` field in mutation items specifies the operation to perform:
| Type | Description |
| -------- | --------------------------------------------------------------- |
| `INSERT` | Create a new edge or update an existing edge with a new version |
| `UPDATE` | Update properties of an existing edge |
| `DELETE` | Delete an edge (marks it as deleted) |
Immutable edge tables accept `INSERT` only; `UPDATE` and `DELETE` are rejected with `400`. Rows are removed via [scan-delete](#5-scan-delete-immutable-edge-tables) instead.
## Lock Parameter
[Section titled “Lock Parameter”](#lock-parameter)
The `lock` parameter controls whether to acquire a lock during mutation:
* **true (default)**: Acquires a lock to prevent concurrent modifications. Ensures data consistency but may have higher latency.
* **false**: Skips locking. Faster but may lead to race conditions if multiple mutations target the same edge simultaneously.
**Note:** It is recommended to use `lock=true` in production environments to ensure data consistency.
## Data Model
[Section titled “Data Model”](#data-model)
### EdgeBulkMutationRequest
[Section titled “EdgeBulkMutationRequest”](#edgebulkmutationrequest)
Edge mutation request payload.
```kotlin
data class EdgeBulkMutationRequest(
val mutations: List, // List of mutation items
) {
data class MutationItem(
val type: EventType, // Event type (INSERT, UPDATE, DELETE)
val edge: Edge, // Edge data
)
}
```
### MultiEdgeBulkMutationRequest
[Section titled “MultiEdgeBulkMutationRequest”](#multiedgebulkmutationrequest)
Multi-edge mutation request payload.
```kotlin
data class MultiEdgeBulkMutationRequest(
val mutations: List, // List of mutation items
) {
data class MutationItem(
val type: EventType, // Event type (INSERT, UPDATE, DELETE)
val edge: MultiEdge, // Multi-edge data
)
}
```
### EdgeMutationResponse
[Section titled “EdgeMutationResponse”](#edgemutationresponse)
Edge mutation response payload.
```kotlin
data class EdgeMutationResponse(
val results: List, // List of mutation results
) {
data class Item(
val source: Any, // Source node ID
val target: Any, // Target node ID
val status: String, // Mutation status (e.g., CREATED, UPDATED, DELETED)
val count: Int, // Number of edges affected
)
}
```
### MultiEdgeMutationResponse
[Section titled “MultiEdgeMutationResponse”](#multiedgemutationresponse)
Multi-edge mutation response payload.
```kotlin
data class MultiEdgeMutationResponse(
val results: List, // List of mutation results
) {
data class Item(
val id: Any, // Edge ID
val status: String, // Mutation status (e.g., CREATED, UPDATED, DELETED)
val count: Int, // Number of edges affected
)
}
```
### Edge
[Section titled “Edge”](#edge)
Individual edge information for mutation.
```kotlin
data class Edge(
val version: Long, // Edge version
val source: Any, // Source node ID
val target: Any, // Target node ID
val properties: Map, // Edge properties
)
```
### MultiEdge
[Section titled “MultiEdge”](#multiedge)
Individual multi-edge information for mutation.
```kotlin
data class MultiEdge(
val version: Long, // Edge version
val id: Any, // Edge ID
val source: Any? = null, // Source node ID (optional)
val target: Any? = null, // Target node ID (optional)
val properties: Map, // Edge properties
)
```
### EdgeScanDeleteResponse
[Section titled “EdgeScanDeleteResponse”](#edgescandeleteresponse)
Scan-delete response payload.
```kotlin
data class EdgeScanDeleteResponse(
val database: String, // Target database name
val table: String, // Target table name
val index: String, // Index that was scanned
val deleted: Int, // Rows deleted by this call (at most `limit`)
)
```
# Query API Reference
> Edge query API reference
Caution
Authentication is not enforced. See [API References](/api-references/#authentication).
Edge query API (unique-edge). For unique-edge vs multi-edge, see [FAQ](/faq/#what-is-the-difference-between-unique-edge-and-multi-edge). For conceptual background, see [Query](/design/query/).
## 1. Count
[Section titled “1. Count”](#1-count)
Retrieve the number of edges starting from a specific node.
### Endpoint
[Section titled “Endpoint”](#endpoint)
```plaintext
GET /graph/v3/databases/{database}/tables/{table}/edges/count
```
### Parameters
[Section titled “Parameters”](#parameters)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------ | ----------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name |
| Query | start | Required | Starting node ID |
| Query | direction | Required | [Query direction](#query-direction) (OUT or IN) |
| Query | ranges | Optional (default: null) | [Scan range](#index-ranges) |
### Response Type
[Section titled “Response Type”](#response-type)
[Count](#count-response) - Payload containing edge count information
### Request Example
[Section titled “Request Example”](#request-example)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| start | source1 |
| direction | OUT |
```bash
# count?start=source1&direction=OUT
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/count?start=source1&direction=OUT" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example)
```json
{
"start": "source1",
"direction": "OUT",
"count": 10,
"context": {}
}
```
## 2. Get
[Section titled “2. Get”](#2-get)
Retrieve an edge between a source node and a target node.
### Endpoint
[Section titled “Endpoint”](#endpoint-1)
```plaintext
GET /graph/v3/databases/{database}/tables/{table}/edges/get
```
### Parameters
[Section titled “Parameters”](#parameters-1)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------ | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name |
| Query | source | Required | Source node ID list (comma-separated) |
| Query | target | Required | Target node ID list (comma-separated) |
| Query | filters | Optional (default: null) | [Filtering conditions](#filters) |
When multiple source or target IDs are specified, the operation behaves as **mget** to retrieve multiple edges. In this case, a maximum of **25 edges** can be retrieved per API request. To retrieve more edges, make multiple API requests.
### Response Type
[Section titled “Response Type”](#response-type-1)
[EdgeFrame](#edgeframe-response) - Payload containing edge data
### Request Example (Get) - 1 source, 1 target
[Section titled “Request Example (Get) - 1 source, 1 target”](#request-example-get---1-source-1-target)
Retrieves edge data for (source, target). (Maximum 1 result)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| source | source1 |
| target | target1 |
```bash
# get?source=source1&target=target1
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/get?source=source1&target=target1" \
-H "Authorization: YOUR_API_KEY"
```
**Response:**
```json
{
"edges": [
{
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
},
"context": {}
}
],
"count": 1,
"total": 1,
"offset": null,
"hasNext": false,
"context": {}
}
```
### Request Example (MGet) - 1 source, N targets
[Section titled “Request Example (MGet) - 1 source, N targets”](#request-example-mget---1-source-n-targets)
Retrieves edge data for (source, target1) \~ (source, targetN). (Maximum N results)
| Parameter | Value |
| ------------- | ----------------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| source | source1 |
| target | target1,target2,target3 |
```bash
# get?source=source1&target=target1,target2,target3
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/get?source=source1&target=target1,target2,target3" \
-H "Authorization: YOUR_API_KEY"
```
**Response:**
```json
{
"edges": [
{
"version": 3,
"source": "source1",
"target": "target3",
"properties": {
"weight": 0.75,
"type": "FOLLOWS"
},
"context": {}
},
{
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
},
"context": {}
}
],
"count": 2,
"total": 2,
"offset": null,
"hasNext": false,
"context": {}
}
```
### Request Example (MGet) - M sources, 1 target
[Section titled “Request Example (MGet) - M sources, 1 target”](#request-example-mget---m-sources-1-target)
Retrieves edge data for (source1, target) \~ (sourceM, target). (Maximum M results)
| Parameter | Value |
| ------------- | ----------------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| source | source1,source2,source3 |
| target | target1 |
```bash
# get?source=source1,source2,source3&target=target1
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/get?source=source1,source2,source3&target=target1" \
-H "Authorization: YOUR_API_KEY"
```
**Response:**
```json
{
"edges": [
{
"version": 3,
"source": "source3",
"target": "target1",
"properties": {
"weight": 0.75,
"type": "FOLLOWS"
},
"context": {}
},
{
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
},
"context": {}
}
],
"count": 2,
"total": 2,
"offset": null,
"hasNext": false,
"context": {}
}
```
### Request Example (MGet) - M sources, N targets (Not Recommended)
[Section titled “Request Example (MGet) - M sources, N targets (Not Recommended)”](#request-example-mget---m-sources-n-targets-not-recommended)
Retrieves edge data for (source1, target1) \~ (sourceM, targetN). (Maximum M × N results)
**Note:** This format should not be used in production as it can retrieve up to M × N edges.
| Parameter | Value |
| ------------- | ----------------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| source | source1,source2,source3 |
| target | target1,target2,target3 |
```bash
# get?source=source1,source2,source3&target=target1,target2,target3
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/get?source=source1,source2,source3&target=target1,target2,target3" \
-H "Authorization: YOUR_API_KEY"
```
**Response:**
```json
{
"edges": [
{
"version": 3,
"source": "source1",
"target": "target3",
"properties": {
"weight": 0.75,
"type": "FOLLOWS"
},
"context": {}
},
{
"version": 3,
"source": "source3",
"target": "target1",
"properties": {
"weight": 0.75,
"type": "FOLLOWS"
},
"context": {}
},
{
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
},
"context": {}
}
],
"count": 3,
"total": 3,
"offset": null,
"hasNext": false,
"context": {}
}
```
## 3. Get (Multi-Edge)
[Section titled “3. Get (Multi-Edge)”](#3-get-multi-edge)
Retrieve multiple multi-edges by their IDs. For unique-edge vs multi-edge, see [FAQ](/faq/#what-is-the-difference-between-unique-edge-and-multi-edge).
### Endpoint
[Section titled “Endpoint”](#endpoint-2)
```plaintext
GET /graph/v3/databases/{database}/tables/{table}/multi-edges/ids
```
### Parameters
[Section titled “Parameters”](#parameters-2)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------------ | ---------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name (must be a multi-edge table) |
| Query | ids | Required | Edge ID list (comma-separated) |
| Query | filters | Optional (default: null) | [Filtering conditions](#filters) |
| Query | features | Optional (default: empty list) | List of features to include |
A maximum of **25 edges** can be retrieved per API request. To retrieve more edges, make multiple API requests.
### Response Type
[Section titled “Response Type”](#response-type-2)
[EdgeFrame](#edgeframe-response) - Payload containing edge data
### Request Example (GET)
[Section titled “Request Example (GET)”](#request-example-get)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| ids | id1,id2,id3 |
```bash
# multi-edges/ids?ids=id1,id2,id3
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/multi-edges/ids?ids=id1,id2,id3" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-1)
```json
{
"edges": [
{
"version": 1,
"source": "id1",
"target": "id1",
"properties": {
"amount": 1000,
"paidAt": 1609459200000
},
"context": {}
},
{
"version": 1,
"source": "id3",
"target": "id3",
"properties": {
"amount": 3000,
"paidAt": 1609545600000
},
"context": {}
}
],
"count": 2,
"total": 2,
"offset": null,
"hasNext": false,
"context": {}
}
```
### Request Example (POST)
[Section titled “Request Example (POST)”](#request-example-post)
For requests with many IDs, use the POST endpoint with a JSON body.
```plaintext
POST /graph/v3/databases/{database}/tables/{table}/multi-edges/ids
```
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
**Request Body:**
```json
{
"ids": ["id1", "id2", "id3"],
"filters": "amount:gte:1000",
"features": []
}
```
```bash
curl -X POST \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/multi-edges/ids" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids": ["id1", "id2", "id3"], "filters": "amount:gte:1000"}'
```
## 4. Scan
[Section titled “4. Scan”](#4-scan)
Scan edges using an index.
### Endpoint
[Section titled “Endpoint”](#endpoint-3)
```plaintext
GET /graph/v3/databases/{database}/tables/{table}/edges/scan/{index}
```
### Parameters
[Section titled “Parameters”](#parameters-3)
| Location | Parameter | Required | Description |
| -------- | ------------- | ------------------------------ | ------------------------------------------------------------------------ |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | database | Required | Target database name |
| Path | table | Required | Target table name |
| Path | index | Required | Index name to use |
| Query | start | Required | Starting node ID |
| Query | direction | Required | [Query direction](#query-direction) (OUT or IN) |
| Query | limit | Optional | Maximum number of results to return; 25 is recommended for production |
| Query | offset | Optional (default: null) | [Pagination](#pagination) offset |
| Query | ranges | Optional (default: null) | [Scan range](#index-ranges) |
| Query | filters | Optional (default: null) | [Filtering conditions](#filters) |
| Query | features | Optional (default: empty list) | List of features to include (e.g., `total` to calculate the total value) |
### Response Type
[Section titled “Response Type”](#response-type-3)
[EdgeFrame](#edgeframe-response) - Payload containing edge data
### Request Example
[Section titled “Request Example”](#request-example-1)
| Parameter | Value |
| ------------- | -------------- |
| Authorization | YOUR\_API\_KEY |
| database | your\_database |
| table | your\_table |
| index | your\_index |
| start | source1 |
| direction | OUT |
```bash
# scan/your_index?start=source1&direction=OUT
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/scan/your_index?start=source1&direction=OUT" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-2)
```json
{
"edges": [
{
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
},
"context": {}
}
],
"count": 1,
"total": -1,
"offset": null,
"hasNext": false,
"context": {}
}
```
**Note:** When `total` is -1, the total count is not calculated. If `features=total` is provided, the total is actually calculated.
## Pagination
[Section titled “Pagination”](#pagination)
The `offset` parameter indicates the starting position of the next page, and `hasNext` indicates whether there is a next page. If there is no offset, the first page is returned.
### 1. First Page Request
[Section titled “1. First Page Request”](#1-first-page-request)
Request the first page without an offset.
**Request Example**
| Parameter | Value | Description |
| ------------- | -------------- | -------------- |
| Authorization | YOUR\_API\_KEY | |
| database | your\_database | |
| table | your\_table | |
| index | your\_index | |
| start | source1 | |
| direction | OUT | |
| limit | | 25 recommended |
```bash
# scan/your_index?start=source1&direction=OUT
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/scan/your_index?start=source1&direction=OUT" \
-H "Authorization: YOUR_API_KEY"
```
**Response:**
```json
{
"edges": [
{
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
},
"context": {}
},
...
],
"count": 10,
"total": -1,
"offset": "string_encoded",
"hasNext": true,
"context": {}
}
```
### 2. Next Page Request
[Section titled “2. Next Page Request”](#2-next-page-request)
Use the `offset` value from the previous response to retrieve the next page.
**Request Example**
| Parameter | Value | Description |
| ------------- | --------------- | ------------------------------------- |
| Authorization | YOUR\_API\_KEY | |
| database | your\_database | |
| table | your\_table | |
| index | your\_index | |
| start | source1 | |
| direction | OUT | |
| offset | string\_encoded | Value received from previous response |
| limit | | 25 recommended |
```bash
# scan/your_index?start=source1&direction=OUT&offset=string_encoded
curl -X GET \
"http://ab.example.com/graph/v3/databases/your_database/tables/your_table/edges/scan/your_index?start=source1&direction=OUT&offset=string_encoded" \
-H "Authorization: YOUR_API_KEY"
```
**Response:**
```json
{
"edges": [
{
"version": 1,
"source": "source1",
"target": "target1",
"properties": {
"weight": 0.8,
"type": "FOLLOWS"
},
"context": {}
},
...
],
"count": 10,
"total": -1,
"offset": null,
"hasNext": false,
"context": {}
}
```
## Index Ranges
[Section titled “Index Ranges”](#index-ranges)
The `ranges` parameter allows you to specify the data scan range in count/scan queries.
### Relationship between Ranges and Indexes
[Section titled “Relationship between Ranges and Indexes”](#relationship-between-ranges-and-indexes)
> **Important:** Ranges can only use fields that are included in the index.
1. **Index Dependency:** Fields used in `ranges` must have an index. Attempting to specify a range on a field without an index will return unexpected results.
2. **Operator Meaning:** The `eq`, `gt(e)`, `lt(e)`, and `between` operators determine the start and end points of the actual storage scan. They must be used appropriately according to index properties.
* When an index is set to `DESC` (descending): `lt` becomes the start point and `gt` becomes the end point.
* When an index is set to `ASC` (ascending): `gt` becomes the start point and `lt` becomes the end point.
3. **Composite Ranges:** Ranges can be used in the order of the field combination that the index is defined on. Ranges must be applied in the order of fields defined in the index.
### Range Syntax
[Section titled “Range Syntax”](#range-syntax)
Ranges are written in the following format:
```plaintext
ranges=range1[;range2;range3;...]
```
Each range follows this format:
```plaintext
field:operator:value
```
To apply multiple ranges, separate them with semicolons (`;`):
```plaintext
field1:operator1:value1;field2:operator2:value2
```
When using the `in` or `bt` (between) operator, multiple values are separated by commas (`,`):
```plaintext
field:in:value1,value2,value3
field:bt:minValue,maxValue
```
### Operators
[Section titled “Operators”](#operators)
| Operator | Description | Example |
| -------- | --------------------- | ------------------------------ |
| `eq` | Equal | `type:eq:0` |
| `gt` | Greater than | `createdAt:gt:1000000` |
| `gte` | Greater than or equal | `createdAt:gte:1000000` |
| `lt` | Less than | `createdAt:lt:2000000` |
| `lte` | Less than or equal | `createdAt:lte:2000000` |
| `bt` | Between two values | `createdAt:bt:1000000,2000000` |
### Examples
[Section titled “Examples”](#examples)
**Simple range:**
```plaintext
GET .../scan/your_index?start=10&direction=OUT&ranges=type:eq:0
```
This example specifies a range to retrieve only edges where the `type` property is 0. Note that `type` refers to `properties.type`, and `properties.type` must be included in `your_index`.
**Composite range:**
```plaintext
GET .../scan/your_index?start=10&direction=OUT&ranges=type:eq:0;createdAt:gt:1000000
```
This example specifies a range to retrieve only edges where `type` is 0 and `createdAt` is greater than 1000000.
**Between operator:**
```plaintext
GET .../scan/your_index?start=10&direction=OUT&ranges=createdAt:bt:1000000,2000000
```
This example specifies a range to retrieve only edges where `createdAt` is between 1000000 and 2000000.
## Filters
[Section titled “Filters”](#filters)
The `filters` parameter allows you to filter data (edges) in get/scan queries. It uses the same format as [ranges](#index-ranges).
**Difference from ranges:** While [ranges](#index-ranges) pre-specifies the query range and can only use fields set as indexes, `filters` filters the query results and can use fields that are not set as indexes.
### Filter Examples
[Section titled “Filter Examples”](#filter-examples)
```plaintext
GET .../get?source=1,2,3&target=10&filters=type:eq:0
GET .../scan/your_index?start=1&direction=OUT&ranges=createdAt:gte:1000000&filters=type:eq:0
```
## Data Model
[Section titled “Data Model”](#data-model)
### Query Direction
[Section titled “Query Direction”](#query-direction)
* **OUT:** Outgoing direction (e.g., retrieving products that a user liked in a user \[likes]→ product relationship)
* **IN:** Incoming direction (e.g., retrieving users who liked a product in the above relationship)
### Count Response
[Section titled “Count Response”](#count-response)
Count query response. Payload containing edge count information.
```kotlin
data class Count(
val start: Any, // Starting node ID
val direction: Direction, // Direction (IN, OUT)
val count: Long, // Edge count
val context: Map, // Context information
)
```
### EdgeFrame Response
[Section titled “EdgeFrame Response”](#edgeframe-response)
Get and scan query response. Payload containing edge data.
```kotlin
data class EdgeFrame(
val edges: List, // Edge list
val count: Int, // Number of edges currently returned
val total: Long, // Total edge count (-1 means not calculated)
val offset: String?, // Pagination offset
val hasNext: Boolean, // Whether next page exists
val context: Map, // Context information
)
```
### Edge
[Section titled “Edge”](#edge)
Individual edge information payload.
```kotlin
data class Edge(
val version: Long, // Edge version
val source: Any, // Source node ID
val target: Any, // Target node ID
val properties: Map, // Edge properties
val context: Map, // Context information
)
```
# Queue API Reference
> queue/v1 API reference
Caution
Authentication is not enforced. See [API References](/api-references/#authentication).
queue/v1 API. See [Queue](/design/queue/) for conceptual background.
## 1. Create Queue
[Section titled “1. Create Queue”](#1-create-queue)
Create a queue, provisioning the backing immutable edge table with the fixed queue schema (`seq`, `value`, and a server-assigned ULID message id).
### Endpoint
[Section titled “Endpoint”](#endpoint)
```plaintext
POST /queue/v1/namespaces/{namespace}/queues
```
### Parameters
[Section titled “Parameters”](#parameters)
| Location | Parameter | Required | Description |
| -------- | ------------- | ---------------------- | ----------------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Body | queue | Required | Queue name |
| Body | storage | Required | Storage URI (e.g., `datastore:///
`) |
| Body | partitions | Optional (default: 30) | Partition count, fixed at creation (minimum 1) |
### Response Type
[Section titled “Response Type”](#response-type)
[QueueDescriptorResponse](#queuedescriptorresponse) - Queue metadata
### Request Example
[Section titled “Request Example”](#request-example)
| Parameter | Value |
| ------------- | --------------- |
| Authorization | YOUR\_API\_KEY |
| namespace | your\_namespace |
```bash
# POST /queue/v1/namespaces/your_namespace/queues
curl -X POST \
"http://ab.example.com/queue/v1/namespaces/your_namespace/queues" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"queue": "your_queue",
"storage": "datastore://your_namespace/your_queue",
"partitions": 30
}'
```
### Response Example
[Section titled “Response Example”](#response-example)
```json
{
"namespace": "your_namespace",
"queue": "your_queue",
"partitions": 30,
"storage": "datastore://your_namespace/your_queue"
}
```
## 2. Get Queue
[Section titled “2. Get Queue”](#2-get-queue)
Get a queue’s metadata.
### Endpoint
[Section titled “Endpoint”](#endpoint-1)
```plaintext
GET /queue/v1/namespaces/{namespace}/queues/{queue}
```
### Parameters
[Section titled “Parameters”](#parameters-1)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Path | queue | Required | Target queue name |
### Response Type
[Section titled “Response Type”](#response-type-1)
[QueueDescriptorResponse](#queuedescriptorresponse) - Queue metadata
### Request Example
[Section titled “Request Example”](#request-example-1)
```bash
# GET /queue/v1/namespaces/your_namespace/queues/your_queue
curl "http://ab.example.com/queue/v1/namespaces/your_namespace/queues/your_queue" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-1)
```json
{
"namespace": "your_namespace",
"queue": "your_queue",
"partitions": 30,
"storage": "datastore://your_namespace/your_queue"
}
```
## 3. Enable / Disable Queue
[Section titled “3. Enable / Disable Queue”](#3-enable--disable-queue)
Activate or deactivate a queue. A queue must be disabled before it can be deleted.
### Endpoint
[Section titled “Endpoint”](#endpoint-2)
```plaintext
PUT /queue/v1/namespaces/{namespace}/queues/{queue}/enable
PUT /queue/v1/namespaces/{namespace}/queues/{queue}/disable
```
### Parameters
[Section titled “Parameters”](#parameters-2)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Path | queue | Required | Target queue name |
### Response Type
[Section titled “Response Type”](#response-type-2)
[QueueDescriptorResponse](#queuedescriptorresponse) - Queue metadata
### Request Example
[Section titled “Request Example”](#request-example-2)
```bash
# PUT /queue/v1/namespaces/your_namespace/queues/your_queue/disable
curl -X PUT \
"http://ab.example.com/queue/v1/namespaces/your_namespace/queues/your_queue/disable" \
-H "Authorization: YOUR_API_KEY"
```
## 4. Delete Queue
[Section titled “4. Delete Queue”](#4-delete-queue)
Delete a queue. The queue must be disabled first; deleting an active queue returns `409 Conflict`.
### Endpoint
[Section titled “Endpoint”](#endpoint-3)
```plaintext
DELETE /queue/v1/namespaces/{namespace}/queues/{queue}
```
### Parameters
[Section titled “Parameters”](#parameters-3)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Path | queue | Required | Target queue name |
### Request Example
[Section titled “Request Example”](#request-example-3)
```bash
# DELETE /queue/v1/namespaces/your_namespace/queues/your_queue
curl -X DELETE \
"http://ab.example.com/queue/v1/namespaces/your_namespace/queues/your_queue" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-2)
`204 No Content` on success; `409 Conflict` if the queue is still enabled.
## 5. Get Partitions
[Section titled “5. Get Partitions”](#5-get-partitions)
Get a queue’s partition count, for consumers that fan a poll loop across `0 .. partitions-1`.
### Endpoint
[Section titled “Endpoint”](#endpoint-4)
```plaintext
GET /queue/v1/namespaces/{namespace}/queues/{queue}/partitions
```
### Parameters
[Section titled “Parameters”](#parameters-4)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Path | queue | Required | Target queue name |
### Response Type
[Section titled “Response Type”](#response-type-3)
[QueuePartitionsResponse](#queuepartitionsresponse) - The partition count
### Request Example
[Section titled “Request Example”](#request-example-4)
```bash
# GET /queue/v1/namespaces/your_namespace/queues/your_queue/partitions
curl "http://ab.example.com/queue/v1/namespaces/your_namespace/queues/your_queue/partitions" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-3)
```json
{
"namespace": "your_namespace",
"queue": "your_queue",
"partitions": 30
}
```
## 6. Enqueue
[Section titled “6. Enqueue”](#6-enqueue)
Append messages to the queue. Each message is routed to a partition by its `key`; the server assigns a ULID id, and `seq` orders it within the partition.
### Endpoint
[Section titled “Endpoint”](#endpoint-5)
```plaintext
POST /queue/v1/namespaces/{namespace}/queues/{queue}/messages
```
### Parameters
[Section titled “Parameters”](#parameters-5)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | ----------------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Path | queue | Required | Target queue name |
| Body | messages | Required | List of messages, each with `key`, `seq`, and `value` |
### Request Body
[Section titled “Request Body”](#request-body)
[EnqueueRequest](#enqueuerequest) - Payload containing messages
### Response Type
[Section titled “Response Type”](#response-type-4)
[EnqueueResponse](#enqueueresponse) - Per-message results
### Request Example
[Section titled “Request Example”](#request-example-5)
```bash
# POST /queue/v1/namespaces/your_namespace/queues/your_queue/messages
curl -X POST \
"http://ab.example.com/queue/v1/namespaces/your_namespace/queues/your_queue/messages" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"key": "user1", "seq": 1000, "value": {"body": "hello"}},
{"key": "user1", "seq": 1001, "value": {"body": "world"}}
]
}'
```
### Response Example
[Section titled “Response Example”](#response-example-4)
```json
{
"accepted": 2,
"results": [
{ "partition": 7, "id": "01JT7Q0Z8B3N5Y6W9XKQRVMH2D", "status": "CREATED" },
{ "partition": 7, "id": "01JT7Q0Z8CM4A1E8ZPXW3G5T7F", "status": "CREATED" }
]
}
```
## 7. Poll
[Section titled “7. Poll”](#7-poll)
Read one partition forward in `seq` order. The response’s `offset` is the cursor for the next poll, and the value to [commit](#8-commit) once the batch is processed.
### Endpoint
[Section titled “Endpoint”](#endpoint-6)
```plaintext
GET /queue/v1/namespaces/{namespace}/queues/{queue}/partitions/{partition}/poll
```
### Parameters
[Section titled “Parameters”](#parameters-6)
| Location | Parameter | Required | Description |
| -------- | ------------- | ----------------------- | --------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Path | queue | Required | Target queue name |
| Path | partition | Required | Partition to read (`0 .. partitions-1`) |
| Query | limit | Optional (default: 100) | Max messages per page (maximum 1000) |
| Query | offset | Optional | Read messages with `seq > offset` (exclusive) |
| Query | until | Optional | Read messages with `seq <= until` (inclusive) |
### Response Type
[Section titled “Response Type”](#response-type-5)
[PollResponse](#pollresponse) - One partition’s page
### Request Example
[Section titled “Request Example”](#request-example-6)
```bash
# GET /queue/v1/namespaces/your_namespace/queues/your_queue/partitions/7/poll
curl "http://ab.example.com/queue/v1/namespaces/your_namespace/queues/your_queue/partitions/7/poll?limit=100&offset=1000" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-5)
```json
{
"messages": [
{
"partition": 7,
"id": "01JT7Q0Z8CM4A1E8ZPXW3G5T7F",
"seq": 1001,
"value": { "body": "world" }
}
],
"offset": 1001,
"hasNext": false
}
```
## 8. Commit
[Section titled “8. Commit”](#8-commit)
Commit a partition up to an offset: every message with `seq <= offset` is deleted. Assumes one logical consumer per partition; see [Queue](/design/queue/#consumer-lifecycle).
### Endpoint
[Section titled “Endpoint”](#endpoint-7)
```plaintext
DELETE /queue/v1/namespaces/{namespace}/queues/{queue}/partitions/{partition}/messages
```
### Parameters
[Section titled “Parameters”](#parameters-7)
| Location | Parameter | Required | Description |
| -------- | ------------- | -------- | -------------------------------------------- |
| Header | Authorization | Optional | Authentication key (reserved for future use) |
| Path | namespace | Required | Target namespace |
| Path | queue | Required | Target queue name |
| Path | partition | Required | Partition to commit (`0 .. partitions-1`) |
| Query | offset | Required | Delete every message with `seq <= offset` |
### Response Type
[Section titled “Response Type”](#response-type-6)
[QueueCommitResponse](#queuecommitresponse) - How many messages were deleted
### Request Example
[Section titled “Request Example”](#request-example-7)
```bash
# DELETE /queue/v1/namespaces/your_namespace/queues/your_queue/partitions/7/messages
curl -X DELETE \
"http://ab.example.com/queue/v1/namespaces/your_namespace/queues/your_queue/partitions/7/messages?offset=1001" \
-H "Authorization: YOUR_API_KEY"
```
### Response Example
[Section titled “Response Example”](#response-example-6)
```json
{
"namespace": "your_namespace",
"queue": "your_queue",
"partition": 7,
"committed": 2
}
```
## Data Model
[Section titled “Data Model”](#data-model)
### QueueCreateRequest
[Section titled “QueueCreateRequest”](#queuecreaterequest)
```kotlin
data class QueueCreateRequest(
val queue: String, // Queue name
val storage: String, // Storage URI
val partitions: Int = 30, // Partition count, fixed at creation (minimum 1)
)
```
### QueueDescriptorResponse
[Section titled “QueueDescriptorResponse”](#queuedescriptorresponse)
```kotlin
data class QueueDescriptorResponse(
val namespace: String,
val queue: String,
val partitions: Int,
val storage: String,
)
```
### QueuePartitionsResponse
[Section titled “QueuePartitionsResponse”](#queuepartitionsresponse)
```kotlin
data class QueuePartitionsResponse(
val namespace: String,
val queue: String,
val partitions: Int,
)
```
### EnqueueRequest
[Section titled “EnqueueRequest”](#enqueuerequest)
```kotlin
data class EnqueueRequest(
val messages: List,
)
data class EnqueueMessage(
val key: String, // Routes to a partition
val seq: Long, // Orders within the partition; may encode a due time
val value: Any? = null // Opaque payload (JSON)
)
```
### EnqueueResponse
[Section titled “EnqueueResponse”](#enqueueresponse)
```kotlin
data class EnqueueResponse(
val accepted: Int, // Count of messages with status CREATED
val results: List,
)
data class EnqueueResult(
val partition: Int, // Partition the message was routed to
val id: String, // Server-assigned ULID
val status: String, // CREATED on success
)
```
### PollResponse
[Section titled “PollResponse”](#pollresponse)
```kotlin
data class PollResponse(
val messages: List, // In seq order
val offset: Long?, // Cursor for the next poll (highest seq seen)
val hasNext: Boolean, // Whether more messages remain in the range
)
data class PolledMessage(
val partition: Int,
val id: String, // ULID message id
val seq: Long,
val value: Any?,
)
```
### QueueCommitResponse
[Section titled “QueueCommitResponse”](#queuecommitresponse)
```kotlin
data class QueueCommitResponse(
val namespace: String,
val queue: String,
val partition: Int,
val committed: Int, // Messages deleted by this commit
)
```