HTTP API Reference
PlugPort exposes a RESTful HTTP API on port 8080 (configurable via HTTP_PORT).
Authentication
PlugPort supports three authentication methods, evaluated in priority order:
1. Session Cookie (SIWE — Recommended)
Sign-In with Ethereum (EIP-4361) provides wallet-based authentication via encrypted cookies.
Flow:
1. POST /api/v1/auth/nonce → { nonce: "abc123" } (stores nonce in session)
2. Sign the SIWE message with your wallet (client-side)
3. POST /api/v1/auth/verify → { address: "0x...", ok: 1 } (sets session cookie + CSRF cookie)
4. All subsequent requests automatically include the session cookie
The session is encrypted using iron-session (AES-256) and stored as an httpOnly cookie. Sessions expire after 24 hours.
2. Wallet-Linked API Key
Generate API keys from the dashboard. Keys are prefixed pp_live_ (production) or pp_test_ (testing):
curl -H "Authorization: Bearer pp_live_your-key-here" http://localhost:8080/api/v1/collections
3. Legacy Static Key
Set the API_KEY environment variable and pass it via header:
API_KEY=your-secret-key pnpm --filter @plugport/server dev
curl -H "x-api-key: your-secret-key" http://localhost:8080/api/v1/collections
CSRF Protection
Session-authenticated (SIWE) mutations require a CSRF token. After /auth/verify, the server sets a plugport_csrf cookie (readable by JS). Include it on all POST/PUT/DELETE requests:
curl -X POST \
-H "Cookie: plugport_session=..." \
-H "x-csrf-token: <value-from-plugport_csrf-cookie>" \
http://localhost:8080/api/v1/collections/users/insertOne \
-d '{"document": {"name": "Alice"}}'
API key and legacy key auth do not require CSRF tokens.
Rate Limits
Auth endpoints have stricter per-route rate limits:
| Endpoint | Rate Limit |
|---|---|
POST /api/v1/auth/nonce | 10/min per IP |
POST /api/v1/auth/verify | 5/min per IP |
GET /api/v1/auth/me | 30/min per IP |
POST /api/v1/auth/logout | 10/min per IP |
| All other endpoints | 100 / 10s per IP |
Auth Endpoints
POST /api/v1/auth/nonce
Request a nonce for SIWE message signing.
Request:
{ "address": "0x1234..." }
Response:
{ "nonce": "abc123def456", "ok": 1 }
POST /api/v1/auth/verify
Verify a signed SIWE message and establish a session.
Request:
{
"message": "plugport.wtf wants you to sign in...",
"signature": "0xabc..."
}
Response:
{ "address": "0x1234...", "ok": 1 }
Sets plugport_session (httpOnly) and plugport_csrf (JS-readable) cookies.
GET /api/v1/auth/me
Check current session status.
Response (authenticated):
{ "address": "0x1234...", "chainId": 10143, "ok": 1 }
Response (not authenticated): 401
POST /api/v1/auth/logout
Destroy the current session and clear cookies.
Response:
{ "ok": 1 }
System Endpoints
GET /health
Returns server health status.
Response:
{
"status": "ok",
"uptime": 12345,
"version": "1.0.0",
"storage": {
"type": "InMemory",
"connected": true,
"keyCount": 1234
},
"server": {
"httpPort": 8080,
"wirePort": 27017
}
}
GET /metrics
Prometheus-format metrics for scraping.
plugport_requests_total{command="find"} 42
plugport_request_duration_ms{quantile="0.95"} 12.5
plugport_errors_total{code="11000"} 3
GET /api/v1/metrics
JSON metrics snapshot for dashboard integration.
Response:
{
"requests": {
"total": 1234,
"byCommand": { "find": 500, "insert": 300, "update": 200 },
"byProtocol": { "http": 1000, "wire": 234 }
},
"latency": { "p50": 2.1, "p95": 12.5, "p99": 45.0, "avg": 5.3 },
"errors": { "total": 12, "byCode": { "11000": 5 } },
"storage": { "keyCount": 5678, "estimatedSizeBytes": 123456 },
"uptime": 3600,
"timestamp": 1708300000000
}
Collection Management
GET /api/v1/collections
List all collections with stats.
Response:
{
"collections": [
{ "name": "users", "documentCount": 100, "indexCount": 3, "createdAt": 1708300000, "ownerAddress": "0x123...", "mode": "public" }
],
"ok": 1
}
POST /api/v1/collections/:name/drop
Drop a collection and all its data.
Response:
{ "acknowledged": true, "dropped": true }
GET /api/v1/collections/:name/stats
Get collection statistics.
Response:
{
"documentCount": 100,
"indexCount": 3,
"storageSizeBytes": 45678,
"indexes": [
{ "name": "_id_", "field": "_id", "unique": true }
]
}
GET /api/v1/collections/:name/privacy
Get privacy settings for a collection. Owners see the full object; non-owners see only mode and ownerAddress.
Response (owner):
{
"ok": 1,
"privacy": {
"mode": "private",
"ownerAddress": "0x123...",
"accessRoles": { "0xabc...": 1 },
"contractAddress": "0xdef..."
}
}
Response (non-owner):
{
"ok": 1,
"privacy": {
"mode": "private",
"ownerAddress": "0x123..."
}
}
POST /api/v1/collections/:name/privacy
Set privacy mode for a collection. Requires authentication. Only the collection owner (or first-time setter) can change the mode.
Request:
{ "mode": "private" }
Response:
{ "ok": 1, "collection": "users", "mode": "private" }
GET /api/v1/collections/:name/roles
View access roles for a collection. Requires authentication. Owner only — returns 403 for non-owners.
Response:
{ "accessRoles": { "0xabc...": 1, "0xdef...": 2 }, "ok": 1 }
Role values: 1 = read, 2 = write.
POST /api/v1/collections/:name/roles
Grant or revoke access roles. Requires authentication. Owner only.
Request (grant):
{ "address": "0xabc...", "action": "grant", "role": 1 }
Request (revoke):
{ "address": "0xabc...", "action": "revoke" }
Response:
{ "accessRoles": { "0xabc...": 1 }, "ok": 1 }
GET /api/v1/whitelist
Get the global address whitelist (public, no auth required).
Response:
{ "addresses": ["0xabc...", "0xdef..."], "ok": 1 }
POST /api/v1/whitelist
Add or remove an address from the global whitelist. Requires authentication.
Request:
{ "address": "0xabc...", "action": "add" }
Response:
{ "ok": 1, "addresses": ["0xabc..."] }
Document Operations
POST /api/v1/collections/:name/insertOne
Insert a single document.
Request:
{
"document": {
"name": "Alice",
"email": "alice@example.com",
"age": 30
}
}
Response:
{
"acknowledged": true,
"insertedId": "67b2a1f0a1b2c3d4e5f6a7b8",
"insertedCount": 1
}
POST /api/v1/collections/:name/insertMany
Insert multiple documents.
Request:
{
"documents": [
{ "name": "Alice", "age": 30 },
{ "name": "Bob", "age": 25 }
]
}
Response:
{
"acknowledged": true,
"insertedCount": 2,
"insertedIds": ["67b2a1f0...", "67b2a1f1..."]
}
POST /api/v1/collections/:name/find
Query documents with filters, sorting, projection, and pagination.
Request:
{
"filter": { "age": { "$gte": 25 } },
"projection": { "name": 1, "age": 1 },
"sort": { "age": -1 },
"limit": 10,
"skip": 0
}
Response:
{
"cursor": {
"firstBatch": [
{ "_id": "67b2a1f0...", "name": "Alice", "age": 30 },
{ "_id": "67b2a1f1...", "name": "Bob", "age": 25 }
],
"id": 0
},
"ok": 1
}
POST /api/v1/collections/:name/findOne
Find a single document.
Request:
{
"filter": { "email": "alice@example.com" }
}
Response:
{
"document": { "_id": "67b2a1f0...", "name": "Alice", "email": "alice@example.com" }
}
Returns { "document": null } if no match found.
POST /api/v1/collections/:name/updateOne
Update a single document.
Request:
{
"filter": { "_id": "67b2a1f0..." },
"update": { "$set": { "age": 31, "updatedAt": "2024-01-01T00:00:00Z" } },
"upsert": false
}
Response:
{
"acknowledged": true,
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": null
}
POST /api/v1/collections/:name/updateMany
Update all documents matching the filter.
Request:
{
"filter": { "status": "inactive" },
"update": { "$set": { "archived": true } }
}
Response:
{
"acknowledged": true,
"matchedCount": 5,
"modifiedCount": 5,
"upsertedId": null
}
POST /api/v1/collections/:name/deleteOne
Delete a single document.
Request:
{ "filter": { "_id": "67b2a1f0..." } }
Response:
{ "acknowledged": true, "deletedCount": 1 }
POST /api/v1/collections/:name/deleteMany
Delete all documents matching the filter.
Request:
{ "filter": { "status": "inactive" } }
Response:
{ "acknowledged": true, "deletedCount": 5 }
Index Operations
POST /api/v1/collections/:name/createIndex
Create an index on a field.
Request:
{ "field": "email", "unique": true }
Response:
{ "acknowledged": true, "indexName": "email_1" }
GET /api/v1/collections/:name/indexes
List all indexes on the collection.
Response:
{
"indexes": [
{ "name": "_id_", "field": "_id", "unique": true },
{ "name": "email_1", "field": "email", "unique": true }
]
}
POST /api/v1/collections/:name/dropIndex
Drop an index by name.
Request:
{ "name": "email_1" }
Response:
{ "acknowledged": true }
POST /api/v1/collections/:name/count
Count documents matching a filter.
Request:
{ "filter": { "status": "active" } }
Response:
{ "count": 42, "ok": 1 }
POST /api/v1/collections/:name/distinct
Get distinct values of a field across documents.
Request:
{ "field": "category", "filter": {} }
Response:
{ "values": ["electronics", "clothing", "books"], "ok": 1 }
POST /api/v1/collections/:name/aggregate
Execute an aggregation pipeline on a collection.
Supported Stages: $match, $lookup, $project, $sort, $limit, $skip, $unwind, $count.
Request:
{
"pipeline": [
{ "$match": { "status": "completed" } },
{ "$lookup": {
"from": "users",
"localField": "userId",
"foreignField": "_id",
"as": "user"
}},
{ "$unwind": "$user" },
{ "$project": { "orderId": 1, "total": 1, "user.name": 1 } },
{ "$sort": { "total": -1 } },
{ "$limit": 10 }
]
}
Response:
{
"cursor": {
"firstBatch": [
{ "_id": "abc123", "orderId": "ORD-001", "total": 99.99, "user": { "name": "Alice" } }
],
"id": 0,
"ns": "plugport.orders"
},
"ok": 1
}
Multi-Protocol Endpoints
PlugPort supports SQL and Redis command interfaces that translate to the underlying document store.
GET /api/v1/protocols
List all protocol frontends and their status.
Response:
{
"protocols": [
{ "name": "mongodb", "enabled": true, "port": 27017 },
{ "name": "postgresql", "enabled": false },
{ "name": "redis", "enabled": true, "port": 6379 }
],
"ok": 1
}
POST /api/v1/sql
Execute a SQL query translated to PlugPort document operations. Supports SELECT, INSERT, UPDATE, DELETE, CREATE INDEX, and DROP INDEX.
Request:
{ "query": "SELECT name, age FROM users WHERE age >= 25 ORDER BY age DESC LIMIT 10" }
Response:
{
"ok": 1,
"result": {
"cursor": {
"firstBatch": [
{ "_id": "...", "name": "Alice", "age": 30 }
],
"id": 0
},
"ok": 1
}
}
POST /api/v1/redis
Execute a Redis command via the RESP protocol translation layer.
Request:
{ "command": ["SET", "mykey", "myvalue"] }
Response:
{ "ok": 1, "result": "OK" }
Supported commands: GET, SET, DEL, MGET, MSET, INCR, DECR, APPEND, STRLEN, SETNX, EXISTS, RENAME, KEYS, TYPE, TTL, PTTL, PERSIST, EXPIRE, PEXPIRE, HSET, HGET, HGETALL, HDEL, HKEYS, HVALS, HEXISTS, HLEN, HMSET, HMGET, LPUSH, RPUSH, LPOP, RPOP, LLEN, LRANGE, SADD, SREM, SMEMBERS, SISMEMBER, SCARD, SUBSCRIBE, PUBLISH, UNSUBSCRIBE, PSUBSCRIBE, PING, INFO, DBSIZE, FLUSHDB, SELECT, AUTH, COMMAND.
GET /api/v1/redis/stream
Server-Sent Events (SSE) stream for Redis Pub/Sub messages.
Query Parameters:
channels(required): Comma-separated list of channels to subscribe to.
Example:
curl -N "http://localhost:8080/api/v1/redis/stream?channels=chat,notifications"
SSE Events:
data: {"channel": "chat", "message": "Hello world"}
data: {"event": "subscribe", "channel": "chat", "count": 1}
POST /api/v1/protocols/:name/enable
Enable a protocol frontend. Requires authentication.
Response:
{ "ok": 1, "protocol": "postgresql", "enabled": true }
POST /api/v1/protocols/:name/disable
Disable a protocol frontend. Requires authentication.
Response:
{ "ok": 1, "protocol": "postgresql", "enabled": false }
API Key Management
All API key endpoints require session authentication (SIWE). Keys are scoped to the authenticated wallet.
POST /api/v1/keys/generate
Generate a new wallet-linked API key.
Request:
{
"label": "Production Backend",
"permissions": ["read", "write"],
"rateLimit": 100
}
Response:
{
"apiKey": "pp_live_abc123...",
"hash": "sha256_hash",
"metadata": {
"label": "Production Backend",
"permissions": ["read", "write"],
"rateLimit": 100,
"createdAt": 1720000000000
},
"ok": 1
}
Important: The full API key is only shown once. Store it securely.
GET /api/v1/keys
List all API keys for the authenticated user.
Response:
{
"keys": [
{
"hash": "sha256_hash",
"label": "Production Backend",
"prefix": "pp_live_abc...",
"permissions": ["read", "write"],
"createdAt": 1720000000000
}
],
"ok": 1
}
DELETE /api/v1/keys/:hash
Revoke an API key. Returns 404 if key not found or not owned by you.
Response:
{ "ok": 1, "revoked": true }
POST /api/v1/keys/:hash/rotate
Rotate an API key — generates a new key value with the same metadata.
Response:
{
"apiKey": "pp_live_newkey...",
"hash": "new_sha256_hash",
"metadata": { "label": "Production Backend", "permissions": ["read", "write"] },
"ok": 1
}
PUT /api/v1/keys/:hash/permissions
Update the permissions of an API key.
Request:
{ "permissions": ["read"] }
Response:
{ "ok": 1, "updated": true }
Permission values: "all", "read", "write", "admin".
GET /api/v1/keys/:hash/analytics
Get usage analytics for a specific API key.
Query Parameters:
days(optional, default: 7): Number of days to include.
Response:
{
"analytics": {
"totalRequests": 1234,
"byEndpoint": { "/api/v1/collections/users/find": 500 },
"byDay": [{ "date": "2026-07-10", "count": 200 }]
},
"ok": 1
}
GET /api/v1/analytics/overview
Get aggregated analytics across all your API keys.
Response:
{
"overview": {
"totalRequests": 5000,
"byKey": [
{ "hash": "sha256_hash", "label": "Production Backend", "count": 3000 }
]
},
"ok": 1
}
Error Responses
All errors follow MongoDB-compatible error codes:
{
"ok": 0,
"code": 11000,
"codeName": "DuplicateKey",
"errmsg": "Duplicate key error: field 'email' value 'alice@example.com'"
}
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | 2 | Bad value / invalid request |
| 401 | 13 | Unauthorized (missing API key) |
| 404 | 26 | Namespace/collection not found |
| 409 | 11000 | Duplicate key violation |
| 413 | 10334 | Document too large |
| 500 | 1 | Internal server error |