Koshi Documentation

Welcome to the Koshi documentation. This guide covers everything from installation to the full API reference. Koshi is a terminal-native, decentralized social network powered by ed25519 cryptography.

๐Ÿ“ฆ Version: 2.0.2 ยท License: MIT ยท Author: game_ryo

Installation

Koshi requires Node.js 18+ and PostgreSQL 15+ (for the server). The CLI can be installed independently.

Global Install (CLI only)

# From npm (requires package to be published)
$ npm install -g @ryopc/koshi

# Or from source
$ git clone https://github.com/ryopc/koshi.git
$ cd koshi
$ npm install
$ npm link

Server Installation

$ git clone https://github.com/ryopc/koshi.git
$ cd koshi
$ npm install

# Create PostgreSQL database
$ createdb koshi

# Run database migrations
$ DATABASE_URL=postgresql://user:pass@localhost:5432/koshi \
  JWT_SECRET=your-secret \
  node src/db/migrate.js

# Start the server
$ DATABASE_URL=postgresql://user:pass@localhost:5432/koshi \
  JWT_SECRET=your-secret \
  node bin/server.js

Or use a .env file:

# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/koshi
JWT_SECRET=your-secret-change-in-production
PORT=3000
NODE_ENV=development

Quick Start

Once the server is running and you have the CLI installed:

# Register a new account (generates ed25519 keypair)
$ kb register alice
โœ” Registration successful!

# Post to the board
$ kb post "Hello, koshi! ๐ŸŒŠ"
โœ” Post created!

# View your feed
$ kb feed --limit=10
๐Ÿ“‹ Koshi Board Feed

# Follow someone
$ kb follow bob

# Live stream
$ kb realtime

Configuration

Credentials are stored in ~/.config/koshi/config.json and ~/.snsrc. The CLI reads these automatically.

VariableDefaultDescription
KOSHI_API_URLhttps://koshi-api.ryopc.f5.siAPI base URL
KOSHI_WS_URLwss://koshi-api.ryopc.f5.siWebSocket URL

CLI Reference

The kb command is the gateway to Koshi. All commands are terminal-native with colored output via Chalk and loading spinners via Ora.

$ kb --help

  ๐Ÿ„ koshi โ€” Terminal-Native Decentralized SNS
  Version 1.2.0

  Commands:
    register   Create a new account with ed25519 keypair
    login      Authenticate using existing keypair
    whoami     Show your profile information
    post       Create a new post on the koshi board
    feed       Display your post feed
    follow     Follow a user
    unfollow   Unfollow a user
    dm         Send a direct message
    dms        View your direct messages
    profile    View a user profile
    search     Search users by username
    realtime   Connect to the real-time event stream
    chat       Real-time interactive DM chat
    edit-profile  Update your own profile
    admin      Admin: users, delete-user, grant, revoke
    help       Show this help message

Auth Commands

kb register <username>

Creates a new account. Generates an ed25519 keypair locally, sends the public key to the server, and stores credentials in ~/.config/koshi/config.json.

$ kb register alice
โ ‹ Generating ed25519 keypair...
โœ” Registration successful!

  Username:  alice
  User ID:   e7d00463-38f8-4d85-8231-6762bb988f7d
  Token:     eyJhbGciOiJIUzI1NiIs...
  โœ“ You are now logged in.

kb login <username>

Authenticates using an existing keypair stored in ~/.snsrc. Signs a challenge with your secret key to prove identity.

$ kb login alice
โ ‹ Signing authentication challenge...
โœ” Login successful!

Post Commands

kb post <message>

Creates a new post on the Koshi board. Content is signed with your ed25519 key. Max 2000 characters.

$ kb post "Hello, koshi! ๐ŸŒŠ"
โ ‹ Signing and posting...
โœ” Post created!

  ID:      550e8400-e29b-41d4-a716-446655440000
  Posted:  7/14/2026, 2:30:00 PM

kb feed [--limit=N]

Displays the post feed. Shows posts from users you follow plus your own. Falls back to global feed when not authenticated.

$ kb feed --limit=5
  ๐Ÿ“‹ Koshi Board Feed (5 posts)
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  Alice @alice
  7/14/2026, 2:30:00 PM
  Hello, koshi! ๐ŸŒŠ

Social Commands

kb follow <username>

$ kb follow bob
โ ‹ Following @bob...
โœ” You are now following @bob!

kb unfollow <username>

$ kb unfollow bob
โ ‹ Unfollowing @bob...
โœ” Unfollowed @bob.

kb profile [username]

View a user's public profile. Defaults to your own profile when no username is given.

$ kb profile alice

  ๐Ÿ‘ค Profile
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  Username:        alice
  Display Name:    Alice
  Bio:             Building the terminal future
  Followers:       42
  Following:       12
  Joined:          7/14/2026

kb search <query>

Search users by username or display name. Query must be at least 2 characters.

$ kb search ali
  ๐Ÿ” Search Results for "ali"
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  alice โ€” Alice

kb whoami

Show your own profile information.

$ kb whoami
  Username:        alice
  Followers:       42
  Following:       12
  Joined:          7/14/2026

Multi-Account Management ๐Ÿ†•

Koshi v2.0 supports managing multiple accounts from a single CLI configuration. Each account has its own ed25519 keypair, Nostr keys (optional), and JWT token.

kb register <username>

Create a new account. The new account is automatically added to the multi-account config and set as active.

kb login [username]

Without arguments, shows an interactive list of saved accounts to choose from. With a username, logs in to that specific account.

$ kb login

  ๐Ÿ”‘ ใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’้ธๆŠž
  1. alice โœ“ โ† ็พๅœจ
  2. bob   โ—‹
  0. ใ‚ญใƒฃใƒณใ‚ปใƒซ

  ็•ชๅทใ‚’้ธๆŠž (1-2): 2
  โ†’ bob ใ‚’้ธๆŠžใ—ใพใ—ใŸ

kb switch [username]

Switch between saved accounts without re-authenticating. Interactive selection if no username is given.

kb accounts

List all saved accounts and their login status.

$ kb accounts

  ๐Ÿ“‹ ใ‚ขใ‚ซใ‚ฆใƒณใƒˆไธ€่ฆง (2ไปถ)
  1. alice  โœ“ ใƒญใ‚ฐใ‚คใƒณๆธˆ  โ† ็พๅœจใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆ
  2. bob    ๆœชใƒญใ‚ฐใ‚คใƒณ

kb account remove <username>

Remove a saved account from the local configuration. Shows confirmation prompt before deleting.

$ kb account remove bob
  ๐Ÿ—‘๏ธ @bob ใ‚’ใƒญใƒผใ‚ซใƒซ่จญๅฎšใ‹ใ‚‰ๅ‰Š้™คใ—ใฆใ‚‚ใ‚ˆใ‚ใ—ใ„ใงใ™ใ‹๏ผŸ (y/N): y
โœ” @bob ใ‚’ใƒญใƒผใ‚ซใƒซ่จญๅฎšใ‹ใ‚‰ๅ‰Š้™คใ—ใพใ—ใŸใ€‚

Account credentials are stored in ~/.config/koshi/config.json in the following format:

{
  "version": "2.0.2",
  "activeUsername": "alice",
  "accounts": {
    "alice": {
      "userId": "uuid",
      "username": "alice",
      "publicKey": "hex...",
      "secretKey": "hex...",
      "token": "jwt...",
      "nostr": { ... }
    },
    "bob": { ... }
  }
}

kb edit-profile --display-name=... --bio=... --avatar-url=...

Update your own profile fields. Only include the flags you want to change.

$ kb edit-profile --display-name="Alice" --bio="Building the terminal future"
โ ‹ Updating profile...
โœ” Profile updated!

  Username:        alice
  Display Name:    Alice
  Bio:             Building the terminal future

Available flags:

--display-name="Your Name"   # Max 64 characters
--bio="About you"            # Max 500 characters
--avatar-url="https://..."   # Max 512 characters, must be valid URL

Message Commands

kb dm <username> <message>

Send a signed direct message to another user.

$ kb dm bob "Hey, how's it going?"
โ ‹ Sending DM to @bob...
โœ” DM sent to @bob!
  ID: 550e8400-e29b-41d4-a716-446655440001

kb dms [--unread] [--limit=N]

View your DM inbox. Use --unread to show only unread messages.

$ kb dms --unread

  ๐Ÿ“จ Unread Messages (2 messages)
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  โ† From: Bob @bob
  7/14/2026, 2:35:00 PM
  Hey Alice! Welcome to koshi ๐ŸŽ‰

Realtime Command

kb realtime

Connects to the WebSocket event stream for live updates. Shows new posts, DMs, and user online/offline events in real time.

$ kb realtime
  ๐Ÿ“ก Connecting to realtime feed...
  โœ“ Connected! Waiting for new posts...

  ๐Ÿ“ New Post
  bob 2:40:00 PM
  Just joined koshi!

Press Ctrl+C to disconnect.

Interactive Chat Command

kb chat <username>

Starts an interactive real-time DM session with another user. Messages are sent and received via WebSocket in real time, with ed25519 signatures for authenticity.

$ kb chat bob
  ๐Ÿ’ฌ Live Chat with bob
  Connect to real-time DMs. Type your message and press Enter.
  Type /exit or press Ctrl+C to quit.

  โœ“ Connected!

  ๐ŸŸข bob is now online
  > Hey Bob! How are you?
  Bob: Hey Alice! I'm great, thanks!
  > 

Features:

Admin Commands

kb admin users

List all registered users with their stats (posts, followers, join date). Admin privileges required.

$ kb admin users
  ๐Ÿ‘ฅ All Users (3 total)
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  alice [ADMIN] โ€” Alice
    ID: e7d00463-...  |  Posts: 42  |  Followers: 12  |  Joined: 7/14/2026

  bob โ€” Bob
    ID: f8e11574-...  |  Posts: 7  |  Followers: 3  |  Joined: 7/14/2026

kb admin user <id|username>

View detailed information about a specific user, including public key, DM count, and admin status. Accepts either a UUID or a username.

kb admin delete-user <username>

Permanently delete a user account and all associated data (posts, follows, DMs โ€” cascade deleted). Uses interactive confirmation.

$ kb admin delete-user bob
  โš ๏ธ  WARNING: This will permanently delete the user and all their data.
  This action cannot be undone.

  Target: @bob
  Type the username to confirm: bob
โœ” User @bob has been permanently deleted.

Use --force flag to skip confirmation for scripting.

kb admin grant <username>

Grant admin privileges to a user.

$ kb admin grant bob
โœ” @bob is now an admin!

kb admin revoke <username>

Revoke admin privileges from a user.

$ kb admin revoke bob
โœ” Admin privileges removed from @bob.

CLI Environment Variables

VariableDefaultDescription
KOSHI_API_URLhttps://koshi-api.ryopc.f5.siAPI base URL for all requests
KOSHI_WS_URLwss://koshi-api.ryopc.f5.siWebSocket URL for realtime

Nostr Integration ๐Ÿ†•

Koshi v2.0 integrates with the Nostr protocol, allowing you to bridge your koshi identity with the broader Nostr network. You can generate Nostr keys, push posts to relays, and pull events from the Nostr ecosystem.

โœจ New in v2.0: Nostr integration enables cross-posting between koshi board and Nostr relays, making your content discoverable beyond the koshi ecosystem.

Key Management

Generate or import Nostr keys (nsec/npub) directly from the CLI:

# Generate a new Nostr keypair
$ kb nostr key generate
โœ” Nostr keypair generated and saved!

  npub: npub1...
  nsec: nsec1...

# Import an existing nsec key
$ kb nostr key import nsec1...

# Show current keys
$ kb nostr key show

# Display public key (npub)
$ kb nostr npub

Pushing Posts to Nostr

Publish your koshi board posts to Nostr relays as kind 1 (short text note) events:

$ kb nostr push --limit=20
โœ” 5/20 posts published to Nostr!

  ๐Ÿ“ก Relay Results:
    โ€ข wss://relay.damus.io โœ“ 5 published
    โ€ข wss://relay.nostr.band โœ“ 5 published

Pulling Events from Nostr

Fetch Nostr events and display them in koshi format:

$ kb nostr pull --limit=50
โœ” Found 12 Nostr events!

  ๐Ÿ“ก Nostr Events (12ไปถ)
  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  nostr:a1b2c3d4 ...
  Hello from Nostr!

Relay Management

Add, remove, list, and test Nostr relay connections:

$ kb nostr relay list
  ๐ŸŒ Nostr Relays (4ไปถ)
    โ€ข wss://relay.damus.io
    โ€ข wss://relay.nostr.band

$ kb nostr relay add wss://relay.example.com
โœ“ ใƒชใƒฌใƒผใ‚’่ฟฝๅŠ ใ—ใพใ—ใŸ: wss://relay.example.com

$ kb nostr relay test wss://relay.damus.io
โœ“ wss://relay.damus.io  Latency: 120ms

P2P Sync ๐Ÿ†•

Koshi v2.0 introduces peer-to-peer synchronization powered by the Hypercore protocol stack. Your posts and DMs are replicated across connected peers using hyperswarm DHT for discovery, enabling offline-first and decentralized data distribution.

โœจ New in v2.0: P2P sync uses hypercore (append-only logs with Merkle tree verification), corestore (hypercore factory), and hyperswarm (DHT-based peer discovery with NAT traversal).

Starting a P2P Node

Launch a P2P node to start syncing with peers:

$ kb p2p start
โœ” P2P node started!

  Status:     Running
  Posts:      42
  DMs:        12
  Peers:      3

$ kb p2p status
  Status:     Running
  Posts:      42
  DMs:        12
  Peers:      3

Data Sync

View synced data and manage replication:

# View synced posts
$ kb p2p sync

# View synced DMs
$ kb p2p dms

# Stop the P2P node
$ kb p2p stop
โœ” P2P node stopped.

Configuration

P2P settings are stored in ~/.config/koshi/config.json under the p2p key:

{
  "p2p": {
    "corestorePath": "/home/user/.config/koshi/corestore",
    "autoSync": false,
    "port": 0
  }
}

Enable automatic P2P startup by setting "autoSync": true in the config.

API Reference

Koshi exposes a RESTful JSON API. All endpoints are prefixed with /api.

Base URLs:
Development: http://localhost:3000/api
Production: https://koshi-api.ryopc.f5.si/api

Authentication

Authenticated endpoints require a JWT Bearer token in the Authorization header:

Authorization: Bearer <token>

POST /api/auth/register

Register a new user with an ed25519 public key. Rate limited to 10 req/min.

MethodPathAuthRate Limit
POST/api/auth/registerNo10/min

Request Body:

{
  "username": "alice",       // 3-32 chars, alphanumeric + underscores
  "publicKey": "abcdef..."   // 64-char hex string (32 bytes)
}

Response (201):

{
  "userId": "e7d00463-38f8-4d85-8231-6762bb988f7d",
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

Errors: 400 (validation), 409 (already registered), 500 (server error)

POST /api/auth/login

Login by signing a challenge with your ed25519 key. The challenge format is koshi:login:{username}.

MethodPathAuthRate Limit
POST/api/auth/loginNo10/min

Request Body:

{
  "username": "alice",
  "signature": "abc123..."   // 128-char hex signature of challenge
}

Response (200):

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "userId": "e7d00463-38f8-4d85-8231-6762bb988f7d"
}

User Endpoints

MethodPathAuthDescription
GET/api/users/:usernameNoGet user profile
PUT/api/users/meYesUpdate own profile
GET/api/users/search/:queryNoSearch users (min 2 chars)
GET/api/users/:id/followersNoGet user's followers
GET/api/users/:id/followingNoGet user's following
POST/api/users/:id/followYesFollow a user
DELETE/api/users/:id/followYesUnfollow a user

GET /api/users/:username

$ curl https://koshi-api.ryopc.f5.si/api/users/alice

{
  "id": "e7d00463-38f8-4d85-8231-6762bb988f7d",
  "username": "alice",
  "displayName": "Alice",
  "bio": "Building the terminal future",
  "avatarUrl": null,
  "followersCount": 42,
  "followingCount": 12,
  "createdAt": "2026-07-14T05:00:00.000Z"
}

PUT /api/users/me

Update profile fields. Only include fields you want to change.

$ curl -X PUT https://koshi-api.ryopc.f5.si/api/users/me \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Alice",
    "bio": "Building the terminal future"
  }'

Post Endpoints

MethodPathAuthDescription
GET/api/posts/feedOptionalGet post feed (user's follows or global)
POST/api/postsYesCreate a new post
GET/api/posts/:idNoGet a single post

GET /api/posts/feed

Returns posts from followed users + own posts (authenticated) or global feed (anonymous).

Query params: ?limit=20 (max 100), ?offset=0

POST /api/posts

Create a new post. Content must be signed with your ed25519 key. Rate limited to 10 posts/min.

$ curl -X POST https://koshi-api.ryopc.f5.si/api/posts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello, koshi! ๐ŸŒŠ",
    "signature": "abc123..."
  }'

Response (201):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "author": {
    "id": "e7d00463-38f8-4d85-8231-6762bb988f7d",
    "username": "alice",
    "displayName": "Alice"
  },
  "content": "Hello, koshi! ๐ŸŒŠ",
  "signature": "abc123...",
  "createdAt": "2026-07-14T05:00:00.000Z"
}

Admin Endpoints

Admin endpoints require authentication AND admin privileges. Admin status is determined by the is_admin flag in the database or the ADMIN_USERNAME environment variable.

MethodPathAuthDescription
GET/api/admin/usersAdminList all users with stats
GET/api/admin/users/:idAdminGet detailed user info โ€” accepts UUID or username
DELETE/api/admin/users/:idAdminPermanently delete a user โ€” accepts UUID or username
PUT/api/admin/users/:id/adminAdminGrant/revoke admin privileges โ€” accepts UUID or username

DELETE /api/admin/users/:id

Permanently deletes a user and all associated data (posts, follows, DMs) via database CASCADE. Accepts either a UUID or a username as the :id parameter.

$ curl -X DELETE https://koshi-api.ryopc.f5.si/api/admin/users/bob \
  -H "Authorization: Bearer <admin-token>"

# Or using a UUID:
$ curl -X DELETE https://koshi-api.ryopc.f5.si/api/admin/users/f8e11574-38f8-4d85-8231-6762bb988f7d \
  -H "Authorization: Bearer <admin-token>"

{
  "success": true,
  "deletedUser": {
    "id": "f8e11574-...",
    "username": "bob"
  }
}

๐Ÿ’ก Tip: All admin user endpoints accept both a UUID or a username in the :id path parameter. The server automatically detects which one you're using.

PUT /api/admin/users/:id/admin

Toggle admin status for a user. Accepts either a UUID or a username as the :id parameter. Body: { "isAdmin": true } or { "isAdmin": false }.

$ curl -X PUT https://koshi-api.ryopc.f5.si/api/admin/users/bob/admin \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"isAdmin": true}'

{
  "success": true,
  "user": {
    "id": "...",
    "username": "bob",
    "isAdmin": true
  }
}

DM Endpoints

MethodPathAuthDescription
GET/api/dmsYesGet DM inbox
POST/api/dms/:userIdYesSend a DM to a user
PUT/api/dms/:id/readYesMark DM as read
GET/api/dms/unread/countYesCount unread DMs

GET /api/dms

Query params: ?limit=50, ?offset=0, ?unread=true

POST /api/dms/:userId

Send a signed direct message. Content max 5000 characters.

$ curl -X POST https://koshi-api.ryopc.f5.si/api/dms/TARGET_USER_ID \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hey there!",
    "signature": "abc123..."
  }'

Health Check

MethodPathDescription
GET/api/healthServer health check (no DB required)
$ curl https://koshi-api.ryopc.f5.si/api/health
{
  "status": "ok",
  "service": "koshi-api",
  "version": "1.2.0",
  "timestamp": "2026-07-14T05:00:00.000Z"
}

Cryptography

Koshi uses ed25519 elliptic curve cryptography for all identity and signing operations. This is the same curve used by the Nostr protocol.

Key Generation

When you register, a new ed25519 keypair is generated locally using tweetnacl. Keys are hex-encoded for storage:

import nacl from 'tweetnacl';

function generateKeypair() {
  const keypair = nacl.sign.keyPair();
  return {
    publicKey: bytesToHex(keypair.publicKey),   // 64 hex chars (32 bytes)
    secretKey: bytesToHex(keypair.secretKey),   // 128 hex chars (64 bytes)
  };
}

Key formats:
Public Key: 64-character hex string (32 bytes)
Secret Key: 128-character hex string (64 bytes: seed + public key)
Signature: 128-character hex string (64 bytes)

Signing & Verification

Messages (posts, DMs) are signed with your secret key using @noble/ed25519. Verification uses your public key.

Signing (CLI side)

import * as ed from '@noble/ed25519';

async function signMessage(message, secretKey) {
  const skBytes = hexToBytes(secretKey);
  const msgBytes = new TextEncoder().encode(message);
  const signature = await ed.sign(msgBytes, skBytes);
  return bytesToHex(signature);
}

Verification (Server side)

async function verifySignature(message, signature, publicKey) {
  const msgBytes = new TextEncoder().encode(message);
  const sigBytes = hexToBytes(signature);
  const pkBytes = hexToBytes(publicKey);
  return await ed.verify(sigBytes, msgBytes, pkBytes);
}

Authentication Flow

The authentication flow works as follows:

  1. Registration: Client generates keypair, sends public key + username to server. Server stores the public key and issues a JWT.
  2. Login: Client signs the challenge string "koshi:login:{username}" with their secret key. Server verifies the signature against the stored public key and issues a JWT.
  3. Subsequent requests: JWT token is sent in the Authorization: Bearer {token} header for all authenticated endpoints.

JWT Tokens

JWTs are signed using HS256 (HMAC with SHA-256) and expire after 24 hours.

{
  // Token payload
  userId: "e7d00463-38f8-4d85-8231-6762bb988f7d",
  username: "alice",
  iat: 1784006345,
  exp: 1784092745  // 24 hours
}

โš  Important: Protect your secret key and JWT secret. The JWT secret (JWT_SECRET) should be a cryptographically random string (32+ bytes). Generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Key Derivation

You can derive the public key from a secret key using derivePublicKey(secretKey). This works for both 32-byte seeds and 64-byte full secret keys.

WebSocket Reference

Koshi supports real-time communication via WebSocket. Connect with your JWT token for authentication.

Connection

# Connect with JWT token
ws://host:port/ws?token=<your-jwt-token>

# Production example
wss://koshi-api.ryopc.f5.si/ws?token=eyJhbGciOiJIUzI1NiIs...

Server Events (received from server)

EventPayloadDescription
connected{}Initial connection confirmation
post_created{ author, content, timestamp }New post on the board
dm_received{ from, content, timestamp }New DM received (targeted to recipient)
user_online{ userId, username }User came online
user_offline{ userId }User went offline
follow_notification{ follower, following }Someone followed you
pong{ timestamp }Heartbeat response

Client Actions (sent to server)

TypePayloadDescription
ping{}Heartbeat ping
post:create{ content, signature }Create a new post
dm:send{ recipientId, content, signature }Send a DM
follow{ userId }Follow a user
unfollow{ userId }Unfollow a user

Example: Create post via WebSocket

// Send
{
  "type": "post:create",
  "payload": {
    "content": "Hello from WebSocket!",
    "signature": "abc123..."
  }
}

// Receive confirmation
{
  "type": "post:created",
  "payload": {
    "id": "550e8400-...",
    "author": { "id": "...", "username": "alice" },
    "content": "Hello from WebSocket!",
    "timestamp": "2026-07-14T05:00:00.000Z"
  }
}

Database Schema

Koshi uses PostgreSQL 15 with the following schema. All tables use UUID primary keys and include timestamps.

Tables

users

Stores user identities with ed25519 public keys.

ColumnTypeConstraints
idUUIDPK, default uuid_generate_v4()
usernameVARCHAR(32)UNIQUE, NOT NULL
public_keyTEXTUNIQUE, NOT NULL
display_nameVARCHAR(64)Nullable
bioTEXTNullable
avatar_urlVARCHAR(512)Nullable
is_adminBOOLEANNOT NULL, default FALSE
created_atTIMESTAMPTZNOT NULL, default NOW()
updated_atTIMESTAMPTZNOT NULL, auto-updated

follows

Tracks user follow relationships.

ColumnTypeConstraints
idUUIDPK
follower_idUUIDFK โ†’ users(id), ON DELETE CASCADE
following_idUUIDFK โ†’ users(id), ON DELETE CASCADE
created_atTIMESTAMPTZNOT NULL
UNIQUE (follower_id, following_id)

kb_posts

Stores signed posts on the Koshi board.

ColumnTypeConstraints
idUUIDPK
author_idUUIDFK โ†’ users(id)
contentTEXTCHECK 1-2000 characters
signatureTEXTNOT NULL
created_atTIMESTAMPTZNOT NULL, indexed DESC

dms

Stores signed direct messages between users.

ColumnTypeConstraints
idUUIDPK
sender_idUUIDFK โ†’ users(id)
recipient_idUUIDFK โ†’ users(id)
contentTEXTCHECK 1-5000 characters
signatureTEXTNOT NULL
is_readBOOLEANDEFAULT FALSE
created_atTIMESTAMPTZNOT NULL

Indexes exist on username, public_key, follower/following IDs, post timestamps, and DM recipient inbox queries.

Deployment

Render.com + Neon.tech (Recommended)

1. Set up Neon.tech Database

  1. Go to Neon.tech and create an account
  2. Create a new project (PostgreSQL 15)
  3. Get your connection string from Connection Details
  4. For production, use the pooled connection string (?pgbouncer=true)

2. Deploy on Render.com

  1. Push the repo to GitHub
  2. Go to Render.com and connect your GitHub repo
  3. Render will auto-detect render.yaml (Blueprint) and create the service
  4. In Render dashboard, add environment variables:
    • DATABASE_URL โ€” your Neon.tech connection string
    • JWT_SECRET โ€” generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
  5. Render will auto-deploy on every push to main

Auto-migration: Koshi runs database migrations automatically on every server startup. Tables are created before the HTTP server starts listening, so no manual migration step is needed.

Server Environment Variables

VariableRequiredDescription
DATABASE_URLโœ…PostgreSQL connection string
JWT_SECRETโœ…Secret key for JWT signing (32+ bytes)
PORTโŒServer port (default: 3000)
NODE_ENVโŒdevelopment or production
LOG_LEVELโŒdebug, info, warn, error
ADMIN_USERNAMEโŒBootstrap the first admin user (e.g., alice)

Docker

# Build the image
$ docker build -t koshi-api .

# Run the container
$ docker run -d \
  --name koshi-api \
  -p 3000:3000 \
  -e DATABASE_URL=postgresql://... \
  -e JWT_SECRET=... \
  koshi-api

Docker Compose

version: '3.8'
services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: koshi
      POSTGRES_PASSWORD: postgres
    volumes:
      - pgdata:/var/lib/postgresql/data

  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://postgres:postgres@db:5432/koshi
      JWT_SECRET: change-this-in-production
    depends_on:
      - db

volumes:
  pgdata:

Security

Rate Limiting

Koshi uses express-rate-limit to protect endpoints:

LimiterRateApplied To
authLimiter10 requests/min per IPRegister & Login
apiLimiter100 requests/min per IPAll API routes
postLimiter10 posts/min per userPost creation

Development

Setup

$ git clone https://github.com/ryopc/koshi.git
$ cd koshi
$ npm install
$ npm run dev    # Auto-reload with nodemon
$ npm run migrate # Run database migrations
$ npm test        # Run tests
$ npm run lint    # Run linter

Project Structure

koshi/
โ”œโ”€โ”€ bin/
โ”‚   โ”œโ”€โ”€ cli.js              # CLI/TUI entry point (kb command)
โ”‚   โ””โ”€โ”€ server.js           # Express + WebSocket server
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ”œโ”€โ”€ auth.js         # Auth routes (register/login)
โ”‚   โ”‚   โ”œโ”€โ”€ users.js        # User management routes
โ”‚   โ”‚   โ”œโ”€โ”€ posts.js        # Posts routes (koshi board)
โ”‚   โ”‚   โ”œโ”€โ”€ dms.js          # Direct messages routes
โ”‚   โ”‚   โ””โ”€โ”€ admin.js        # Admin routes (users, delete, grant)
โ”‚   โ”œโ”€โ”€ auth/
โ”‚   โ”‚   โ”œโ”€โ”€ ed25519.js      # Ed25519 crypto utilities
โ”‚   โ”‚   โ”œโ”€โ”€ jwt.js          # JWT token utilities
โ”‚   โ”‚   โ””โ”€โ”€ utils.js        # Hex encoding utilities
โ”‚   โ”œโ”€โ”€ db/
โ”‚   โ”‚   โ”œโ”€โ”€ schema.sql      # PostgreSQL schema
โ”‚   โ”‚   โ”œโ”€โ”€ migrate.js      # Migration script
โ”‚   โ”‚   โ””โ”€โ”€ pool.js         # Database connection pool
โ”‚   โ”œโ”€โ”€ middleware/
โ”‚   โ”‚   โ”œโ”€โ”€ auth.js         # Auth middleware
โ”‚   โ”‚   โ””โ”€โ”€ rateLimit.js    # Rate limiting
โ”‚   โ”œโ”€โ”€ ws/
โ”‚   โ”‚   โ”œโ”€โ”€ index.js        # WebSocket server setup
โ”‚   โ”‚   โ””โ”€โ”€ handlers.js     # WebSocket message handlers
โ”‚   โ””โ”€โ”€ index.js            # Express app setup
โ”œโ”€โ”€ Dockerfile
โ”œโ”€โ”€ render.yaml             # Render Blueprint config
โ””โ”€โ”€ package.json