Overview
This document specifies the Subscriptions gRPC service that lets clients subscribe and unsubscribe to document paths within an account, and list active subscriptions. Subscriptions can be recursive (covering an entire directory) and may be created asynchronously to avoid blocking on initial sync.
Goals
Provide a minimal API to subscribe/unsubscribe to resources identified by (account, path).
Support recursive subscriptions to monitor entire directories.
Offer pagination for listing active subscriptions.
Allow async fire‑and‑forget creation to decouple UI from initial sync.
Non‑Goals
Delivering the event stream itself (handled by Activity/Feed or Sync layers).
Authorization policy definition (capability checks are enforced elsewhere).
Batch subscribe/unsubscribe (can be added later).
Protocol Definition
syntax = "proto3";
package com.seed.activity.v1alpha;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
option go_package = "seed/backend/genproto/activity/v1alpha;activity";
// Subscriptions service provides subscription capabilities.
service Subscriptions {
// Subscribe to a document or space.
rpc Subscribe(SubscribeRequest) returns (google.protobuf.Empty);
// Remove a subscription.
rpc Unsubscribe(UnsubscribeRequest) returns (google.protobuf.Empty);
// Lists active subscriptions.
rpc ListSubscriptions(ListSubscriptionsRequest) returns (ListSubscriptionsResponse);
}
// Subscribe to a resource
message SubscribeRequest {
// Required. The ID of the account where the subscribed document is located.
string account = 1;
// Required. Path of the document.
// Empty string means root document.
string path = 2;
// Optional. Indicate if we not only subscribe to the resource
// ID above but also to all documents on its directory.
bool recursive = 3;
// Optional. If true, the server will not wait for the subscription
// to be synced for the first time before returning.
optional bool async = 4;
}
// Subscribe to a resource
message UnsubscribeRequest {
// Required. The ID of the account where the subscribed document is located.
string account = 1;
// Required. Path of the document.
// Empty string means root document.
string path = 2;
}
// Get a list of active subscriptions.
message ListSubscriptionsRequest {
// Optional. The size of the page. The default is defined by the server.
int32 page_size = 1;
// Optional. The page token for requesting next pages.
string page_token = 2;
}
// Get a list of active subscriptions.
message ListSubscriptionsResponse {
// The list of subscriptions.
repeated Subscription subscriptions = 1;
// The token to request the next page.
string next_page_token = 2;
}
// Description of the subscription item.
message Subscription {
// Account to which the document belongs.
string account = 1;
// Path of the document within the account.
// Empty string means root document.
string path = 2;
// Whether this subscription also subscribes to
// all documents in the document's directory.
bool recursive = 3;
// Timestamp when the user started the subscrition.
google.protobuf.Timestamp since = 4;
}
Service
Subscriptions exposes three RPCs:
Subscribe(SubscribeRequest) → Empty — Create (or idempotently ensure) a subscription for (account, path), optionally recursive and async.
Unsubscribe(UnsubscribeRequest) → Empty — Remove the subscription for (account, path).
ListSubscriptions(ListSubscriptionsRequest) → ListSubscriptionsResponse — Paginated listing of current subscriptions.
All methods are idempotent: re‑subscribing to the same target or unsubscribing a non‑existent subscription should succeed with OK.
Messages
SubscribeRequest
account (string, required) — Account that owns the document space.
path (string, required) — Path in the account; "" denotes root.
recursive (bool, optional) — If true, subscribe to the entire directory subtree.
async (bool, optional) — If true, return immediately without waiting for initial sync.
UnsubscribeRequest
account (string, required) — Account id.
path (string, required) — Path; "" for root.
ListSubscriptionsRequest
page_size (int32, optional) — Page size; server may cap.
page_token (string, optional) — Opaque token to continue pagination.
ListSubscriptionsResponse
subscriptions (Subscription[]) — Page of subscriptions.
next_page_token (string) — Present if more results exist.
Subscription
account, path — Subscription target.
recursive — Whether it applies to a subtree.
since — Creation timestamp.
Field Semantics & Validation
Path normalization — Servers MUST canonicalize paths; treat "" as root.
Idempotence — Repeated Subscribe or Unsubscribe calls for the same target should be safe.
Async behavior — When async=true, the server SHOULD enqueue subscription work and return immediately; otherwise it MAY block until the initial snapshot/sync is confirmed or a deadline is reached.
Recursive semantics — recursive=true applies to all documents under the directory prefix; implementations SHOULD guard against pathological fan‑out.
Uniqueness — A subscription is uniquely identified by (account, path, recursive) or (account, path) depending on implementation; servers SHOULD document which dimensions define uniqueness.
Errors
Common gRPC status codes:
INVALID_ARGUMENT — Malformed account or path; unsupported path.
NOT_FOUND — Unsubscribe target not known (optional; may still return OK for idempotence).
PERMISSION_DENIED — Caller lacks rights to subscribe to the target (e.g., no read capability).
RESOURCE_EXHAUSTED — Subscription limit reached (per user/account/system).
DEADLINE_EXCEEDED — Synchronous Subscribe timed out during initial sync.
UNAVAILABLE — Sync subsystem temporarily unavailable.
INTERNAL — Unexpected server error.
Security & Permissions
AuthZ check — Enforce read access to the target (account, path) prior to creating a subscription; recursive requests must verify subtree access.
Privacy — Avoid revealing existence of private paths via error timing or detailed messages.
Rate limiting — Apply quotas to prevent abuse (especially with recursive=true).
Versioning Strategy
Additive fields to Subscription and request messages are backwards compatible.
Breaking changes require a new package (e.g., v1beta).
Examples
grpcurl — Subscribe (recursive)
grpcurl \
-H "Authorization: Bearer $TOKEN" \
api.seed.hyper.media:443 com.seed.activity.v1alpha.Subscriptions.Subscribe \
'{"account":"acc_1abc","path":"/docs","recursive":true}'
grpcurl — Subscribe (async)
grpcurl \
-H "Authorization: Bearer $TOKEN" \
api.seed.hyper.media:443 com.seed.activity.v1alpha.Subscriptions.Subscribe \
'{"account":"acc_1abc","path":"/docs/roadmap","async":true}'
grpcurl — Unsubscribe
grpcurl \
-H "Authorization: Bearer $TOKEN" \
api.seed.hyper.media:443 com.seed.activity.v1alpha.Subscriptions.Unsubscribe \
'{"account":"acc_1abc","path":"/docs"}'
grpcurl — ListSubscriptions
grpcurl \
-H "Authorization: Bearer $TOKEN" \
api.seed.hyper.media:443 com.seed.activity.v1alpha.Subscriptions.ListSubscriptions \
'{"page_size":50}'
Sample response (truncated):
{
"subscriptions": [
{"account":"acc_1abc","path":"/docs","recursive":true,"since":"2025-10-14T15:00:00Z"},
{"account":"acc_1abc","path":"/announcements","recursive":false,"since":"2025-10-10T12:30:00Z"}
],
"next_page_token": "eyJvZmZzZXQiOjUwLCJjdHMiOiIyMDI1LTEwLTE0VDE1OjAwOjAwWiJ9"
}
Client Guidelines
Prefer async=true for UI flows where initial sync may take time; poll Activity/Feed for updates.
De‑duplicate local subscription state by (account, path) and reconcile with server list.
Use pagination for large subscription sets and avoid hammering with frequent full listings.
Server Guidelines
Enforce idempotence and canonical storage keys for subscriptions.
Respect caller capabilities; deny recursive subscriptions that exceed scope.
Emit internal metrics (creation latency, fan‑out size, queue depth) for ops visibility.
Future Work
Batch operations: BatchSubscribe, BatchUnsubscribe.
Filters for ListSubscriptionsRequest (by account, prefix, recursive flag).
Watch stream for live subscription state changes.
Ownership and labels on subscriptions for UX discoverability.
Changelog
2025‑10‑27: Second draft of Subscriptions service spec (Activity v1alpha).