Skip to content

Resources

A resource is a place items live. It is an instance — one bucket, one mailbox — and its type is the STI subclass it belongs to.

app/models/resource.rb, app/models/resource/

Identity is (tenant_id, type, key); key is validated unique within that pair. sti_name dasherizes the class name, so Resource::OauthGoogle stores and reports as oauth-google.

Type Class Capabilities Syncs
s3 Resource::S3 storage
filesystem Resource::Filesystem storage
database Resource::Database storage
webdav Resource::Webdav storage
imap Resource::Imap
rss Resource::Rss
caldav Resource::Caldav
carddav Resource::Carddav
oauth-google Resource::OauthGoogle integration
openai-compatible Resource::OpenaiCompatible inference

Caldav and Carddav subclass Webdav and inherit its each_page, narrowing what they walk with a wanted? predicate.

Resource::Brokered is a concern, not a type. Resource::OauthGoogle is the only type that includes it — see Enrollment below.

One class serves every vendor speaking its dialect: s3 covers AWS, R2, B2, Wasabi, MinIO and Garage.

self.capabilities returns an array of symbols. storage, integration and inference are in use; transport is defined and unimplemented.

via_id is a nullable self-reference: this resource is reached through that one. Null means reached directly. A resource named as via must declare the transport capability and implement reach!(url), which rewrites the URL its dependent would otherwise dial.

The edge is validated both ways. A via must be this tenant’s, must not be the resource itself, must not close a loop, and must not sit more than MAX_HOPS away. A transport that something still rides on cannot be archived, and the database refuses to delete it or to point a via at another tenant — the foreign key carries tenant_id, so a cross-tenant transport is unrepresentable rather than merely refused.

storage? gates two items: being an export destination (Resource#storage!) and being the tenant’s default_storage, which Resource#make_default_storage! sets exclusively.

Resource#syncable? is respond_to?(:each_page). There is no flag and no registry — a type either implements the enumerator or it cannot sync.

Only a syncable resource may hold a sync_interval, enforced by the only_a_syncable_resource_keeps_a_schedule validation.

MINIMUM_SYNC_INTERVAL 1 minute
SYNC_ABANDONED_AFTER 6 hours — after which sync_started_at no longer counts as a lock

claim_sync! takes the lock with a conditional UPDATE, so a resource already syncing is left alone rather than started twice. release_sync! clears it and computes next_sync_at.

self.command_schema maps a command name to its parameter types; ? marks a parameter optional. Resource#command validates arguments against the schema and dispatches to command_<name>.

app/models/resource/s3.rb
{
list: { prefix: "string?", continuation_token: "string?" },
get: { key: "string", version_id: "string?" },
put: { key: "string", body: "bytes" }
}

describe_resource returns this schema, so a caller discovers a type’s vocabulary rather than having it written down. See MCP tools.

check! is implemented per type and is cheap — a HEAD rather than a listing. check wraps it, writes checked_at and check_error, and returns a boolean. healthy? reads those columns.

Adapters raise Resource::Failed (or a subclass — Resource::Filesystem::Escaped, Resource::Rss::Gone) rather than a vendor error. Jobs retry it with backoff; see Jobs.

Types that include Resource::Brokered hold a connection id, not a credential. Broker.release exchanges it for an access token at the issuer’s /connections/token endpoint on each use.

Enrollment.open! builds a signed, 30-minute (Enrollment::WINDOW) token; /enroll/:token is where the browser completes it. Nothing is created until the link is followed.

Credentials never travel through an MCP tool call.