Appwrite Ruby SDK skill. Use when building server-side Ruby applications with Appwrite, including Rails and Sinatra integrations. Covers user management, database/table CRUD, file storage, and functions via API keys.
npx claudepluginhub joshuarweaver/cascade-data-storage --plugin appwrite-agent-skillsThis skill uses the workspace's default tool permissions.
```bash
Conducts multi-round deep research on GitHub repos via API and web searches, generating markdown reports with executive summaries, timelines, metrics, and Mermaid diagrams.
Dynamically discovers and combines enabled skills into cohesive, unexpected delightful experiences like interactive HTML or themed artifacts. Activates on 'surprise me', inspiration, or boredom cues.
Generates images from structured JSON prompts via Python script execution. Supports reference images and aspect ratios for characters, scenes, products, visuals.
gem install appwrite
require 'appwrite'
include Appwrite
client = Client.new
.set_endpoint('https://<REGION>.cloud.appwrite.io/v1')
.set_project(ENV['APPWRITE_PROJECT_ID'])
.set_key(ENV['APPWRITE_API_KEY'])
users = Users.new(client)
# Create user
user = users.create(user_id: ID.unique, email: 'user@example.com', password: 'password123', name: 'User Name')
# List users
list = users.list(queries: [Query.limit(25)])
# Get user
fetched = users.get(user_id: '[USER_ID]')
# Delete user
users.delete(user_id: '[USER_ID]')
Note: Use
TablesDB(not the deprecatedDatabasesclass) for all new code. Only useDatabasesif the existing codebase already relies on it or the user explicitly requests it.Tip: Prefer keyword arguments (e.g.,
database_id: '...') for all SDK method calls. Only use positional arguments if the existing codebase already uses them or the user explicitly requests it.
tables_db = TablesDB.new(client)
# Create database
db = tables_db.create(database_id: ID.unique, name: 'My Database')
# Create row
doc = tables_db.create_row(
database_id: '[DATABASE_ID]',
table_id: '[TABLE_ID]',
row_id: ID.unique,
data: { title: 'Hello World' }
)
# Query rows
results = tables_db.list_rows(
database_id: '[DATABASE_ID]',
table_id: '[TABLE_ID]',
queries: [Query.equal('title', 'Hello World'), Query.limit(10)]
)
# Get row
row = tables_db.get_row(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', row_id: '[ROW_ID]')
# Update row
tables_db.update_row(
database_id: '[DATABASE_ID]',
table_id: '[TABLE_ID]',
row_id: '[ROW_ID]',
data: { title: 'Updated' }
)
# Delete row
tables_db.delete_row(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', row_id: '[ROW_ID]')
Note: The legacy
stringtype is deprecated. Use explicit column types for all new columns.
| Type | Max characters | Indexing | Storage |
|---|---|---|---|
varchar | 16,383 | Full index (if size ≤ 768) | Inline in row |
text | 16,383 | Prefix only | Off-page |
mediumtext | 4,194,303 | Prefix only | Off-page |
longtext | 1,073,741,823 | Prefix only | Off-page |
varchar is stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.text, mediumtext, and longtext are stored off-page (only a 20-byte pointer lives in the row), so they don't consume the row size budget. size is not required for these types.# Create table with explicit string column types
tables_db.create_table(
database_id: '[DATABASE_ID]',
table_id: ID.unique,
name: 'articles',
columns: [
{ key: 'title', type: 'varchar', size: 255, required: true }, # inline, fully indexable
{ key: 'summary', type: 'text', required: false }, # off-page, prefix index only
{ key: 'body', type: 'mediumtext', required: false }, # up to ~4 M chars
{ key: 'raw_data', type: 'longtext', required: false }, # up to ~1 B chars
]
)
# Filtering
Query.equal('field', 'value') # == (or pass array for IN)
Query.not_equal('field', 'value') # !=
Query.less_than('field', 100) # <
Query.less_than_equal('field', 100) # <=
Query.greater_than('field', 100) # >
Query.greater_than_equal('field', 100) # >=
Query.between('field', 1, 100) # 1 <= field <= 100
Query.is_null('field') # is null
Query.is_not_null('field') # is not null
Query.starts_with('field', 'prefix') # starts with
Query.ends_with('field', 'suffix') # ends with
Query.contains('field', 'sub') # contains
Query.search('field', 'keywords') # full-text search (requires index)
# Sorting
Query.order_asc('field')
Query.order_desc('field')
# Pagination
Query.limit(25) # max rows (default 25, max 100)
Query.offset(0) # skip N rows
Query.cursor_after('[ROW_ID]') # cursor pagination (preferred)
Query.cursor_before('[ROW_ID]')
# Selection & Logic
Query.select(['field1', 'field2']) # return only specified fields
Query.or([Query.equal('a', 1), Query.equal('b', 2)]) # OR
Query.and([Query.greater_than('age', 18), Query.less_than('age', 65)]) # AND (default)
storage = Storage.new(client)
# Upload file
file = storage.create_file(bucket_id: '[BUCKET_ID]', file_id: ID.unique, file: InputFile.from_path('/path/to/file.png'))
# List files
files = storage.list_files(bucket_id: '[BUCKET_ID]')
# Delete file
storage.delete_file(bucket_id: '[BUCKET_ID]', file_id: '[FILE_ID]')
InputFile.from_path('/path/to/file.png') # from filesystem path
InputFile.from_string('Hello world', 'hello.txt') # from string content
teams = Teams.new(client)
# Create team
team = teams.create(team_id: ID.unique, name: 'Engineering')
# List teams
list = teams.list
# Create membership (invite user by email)
membership = teams.create_membership(
team_id: '[TEAM_ID]',
roles: ['editor'],
email: 'user@example.com'
)
# List memberships
members = teams.list_memberships(team_id: '[TEAM_ID]')
# Update membership roles
teams.update_membership(team_id: '[TEAM_ID]', membership_id: '[MEMBERSHIP_ID]', roles: ['admin'])
# Delete team
teams.delete(team_id: '[TEAM_ID]')
Role-based access: Use
Role.team('[TEAM_ID]')for all team members orRole.team('[TEAM_ID]', 'editor')for a specific team role when setting permissions.
functions = Functions.new(client)
# Execute function
execution = functions.create_execution(function_id: '[FUNCTION_ID]', body: '{"key": "value"}')
# List executions
executions = functions.list_executions(function_id: '[FUNCTION_ID]')
# src/main.rb — Appwrite Function entry point
def main(context)
# context.req.body — raw body (String)
# context.req.body_json — parsed JSON (Hash or nil)
# context.req.headers — headers (Hash)
# context.req.method — HTTP method
# context.req.path — URL path
# context.req.query — query params (Hash)
context.log("Processing: #{context.req.method} #{context.req.path}")
if context.req.method == 'GET'
return context.res.json({ message: 'Hello from Appwrite Function!' })
end
context.res.json({ success: true }) # JSON
# context.res.text('Hello') # plain text
# context.res.empty # 204
# context.res.redirect('https://...') # 302
end
SSR apps using Ruby frameworks (Rails, Sinatra, etc.) use the server SDK to handle auth. You need two clients:
require 'appwrite'
include Appwrite
# Admin client (reusable)
admin_client = Client.new
.set_endpoint('https://<REGION>.cloud.appwrite.io/v1')
.set_project('[PROJECT_ID]')
.set_key(ENV['APPWRITE_API_KEY'])
# Session client (create per-request)
session_client = Client.new
.set_endpoint('https://<REGION>.cloud.appwrite.io/v1')
.set_project('[PROJECT_ID]')
session = cookies['a_session_[PROJECT_ID]']
session_client.set_session(session) if session
post '/login' do
account = Account.new(admin_client)
session = account.create_email_password_session(
email: params[:email],
password: params[:password]
)
# Cookie name must be a_session_<PROJECT_ID>
response.set_cookie('a_session_[PROJECT_ID]', {
value: session.secret,
httponly: true,
secure: true,
same_site: :strict,
path: '/',
})
content_type :json
{ success: true }.to_json
end
get '/user' do
session = request.cookies['a_session_[PROJECT_ID]']
halt 401, { error: 'Unauthorized' }.to_json unless session
session_client = Client.new
.set_endpoint('https://<REGION>.cloud.appwrite.io/v1')
.set_project('[PROJECT_ID]')
.set_session(session)
account = Account.new(session_client)
user = account.get
content_type :json
user.to_json
end
# Step 1: Redirect to OAuth provider
get '/oauth' do
account = Account.new(admin_client)
redirect_url = account.create_o_auth2_token(
provider: OAuthProvider::GITHUB,
success: 'https://example.com/oauth/success',
failure: 'https://example.com/oauth/failure'
)
redirect redirect_url
end
# Step 2: Handle callback — exchange token for session
get '/oauth/success' do
account = Account.new(admin_client)
session = account.create_session(
user_id: params[:userId],
secret: params[:secret]
)
response.set_cookie('a_session_[PROJECT_ID]', {
value: session.secret,
httponly: true, secure: true, same_site: :strict, path: '/',
})
content_type :json
{ success: true }.to_json
end
Cookie security: Always use
httponly,secure, andsame_site: :strictto prevent XSS. The cookie name must bea_session_<PROJECT_ID>.
Forwarding user agent: Call
session_client.set_forwarded_user_agent(request.user_agent)to record the end-user's browser info for debugging and security.
require 'appwrite'
include Appwrite
begin
row = tables_db.get_row(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', row_id: '[ROW_ID]')
rescue Appwrite::Exception => e
puts e.message # human-readable message
puts e.code # HTTP status code (Integer)
puts e.type # error type (e.g. 'document_not_found')
puts e.response # full response body (Hash)
end
Common error codes:
| Code | Meaning |
|---|---|
401 | Unauthorized — missing or invalid session/API key |
403 | Forbidden — insufficient permissions |
404 | Not found — resource does not exist |
409 | Conflict — duplicate ID or unique constraint |
429 | Rate limited — too many requests |
Appwrite uses permission strings to control access to resources. Each permission pairs an action (read, update, delete, create, or write which grants create + update + delete) with a role target. By default, no user has access unless permissions are explicitly set at the document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the Permission and Role helpers.
# Permission and Role are included in the main require
require 'appwrite'
include Appwrite
doc = tables_db.create_row(
database_id: '[DATABASE_ID]',
table_id: '[TABLE_ID]',
row_id: ID.unique,
data: { title: 'Hello World' },
permissions: [
Permission.read(Role.user('[USER_ID]')), # specific user can read
Permission.update(Role.user('[USER_ID]')), # specific user can update
Permission.read(Role.team('[TEAM_ID]')), # all team members can read
Permission.read(Role.any), # anyone (including guests) can read
]
)
file = storage.create_file(
bucket_id: '[BUCKET_ID]',
file_id: ID.unique,
file: InputFile.from_path('/path/to/file.png'),
permissions: [
Permission.read(Role.any),
Permission.update(Role.user('[USER_ID]')),
Permission.delete(Role.user('[USER_ID]')),
]
)
When to set permissions: Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document permissions empty.
Common mistakes:
- Forgetting permissions — the resource becomes inaccessible to all users (including the creator)
Role.anywithwrite/update/delete— allows any user, including unauthenticated guests, to modify or remove the resourcePermission.read(Role.any)on sensitive data — makes the resource publicly readable