Skip to content

Collections

Collections are the primary way to structure and organize your data in VSKI. Each collection has a schema that defines the fields and their types.

Collections can live in in different databases, but collection metadata is stored in the default database.

Collections are declared, not created imperatively: the schema lives in YAML and vski apply turns changes into reviewable, rollback-able server-side migrations (see IaC).

vski/default/collections.yaml
version: 1
database: default
collections:
posts:
type: base
fields:
- {name: title, type: text, required: true}
- {name: content, type: text}
- {name: published, type: bool}
- {name: views, type: number}
Terminal window
vski plan # + collection default/posts (create base table with 4 fields)
vski apply # creates it via a migration

Scaffolding a new project? vski setup writes a starter file exactly like this one.

TypeDescriptionExample
textPlain text string"Hello"
emailEmail address"user@example.com"
numberNumeric value42 or 3.14
boolBoolean valueTrue or False
dateDate string"2024-01-01"
datetimeDatetime with timezone"2024-01-01T00:00:00Z"
selectPredefined optionsUse with values: [...]
urlURL string"https://example.com"
editorRich text contentHTML/Markdown content
jsonJSON object{ "key": "value" }
relationReference to another collectionUse with collectionId
collections:
_users:
type: auth
fields:
- {name: email, type: email, required: true}
- {name: password, type: text, required: true}
- {name: name, type: text}

Auth collections automatically include system fields (id, created, updated). Fields whose names start with _ are internal and never managed by IaC.

When declaring a collection you write fields:; when reading collection metadata back the server returns the schema as schema:. Same data, different key on the wire.

Runtime data operations use the Python SDK (pip install vski-sdk; see SDK Installation):

import asyncio
from vski import VskiClient
async def main():
async with VskiClient("http://127.0.0.1:3000") as client:
res = await client.admins.auth_with_password(
"admin@yourdomain.com", "your-password"
)
client.set_token(res["token"])
post = await client.collection("posts").create(
{"title": "Hello World", "content": "My first post",
"published": True, "views": 0}
)
print(post["id"]) # auto-generated
print(post["created"]) # auto timestamp
asyncio.run(main())

For quick one-offs the command line does the same:

Terminal window
vski records create posts '{"title": "Hello World", "published": true}'
vski records list posts --filter 'published = true'
post = await client.collection("posts").create(
{"id": "custom-post-id", "title": "Custom ID Post", "content": "..."}
)
post = await client.collection("posts").get_one("record-id")
print(post["title"])
page = await client.collection("posts").get_list(1, 20)
print(page["totalItems"]) # total number of records
print(page["items"]) # records on this page
print(page["totalPages"]) # total pages
page = await client.collection("posts").get_list(
1, 20, filter="published = true AND views > 10"
)
newest = await client.collection("posts").get_list(1, 20, sort="-created")
multi = await client.collection("posts").get_list(1, 20, sort="-published,created")
updated = await client.collection("posts").update(
"record-id", {"title": "Updated Title", "views": 100}
)
# updates are partial: unchanged fields keep their values
partial = await client.collection("posts").update("record-id", {"title": "Just the title"})
ok = await client.collection("posts").delete("record-id") # True

Filters are SQL-like expression strings:

get = lambda **kw: client.collection("posts").get_list(1, 20, **kw)
# Comparison: = != > < >= <=
await get(filter="views > 100")
# Logical: AND, OR, parentheses for grouping
await get(filter="(status = 'active' OR status = 'pending') AND published = true")
# Strings: ~ is a substring match (LIKE with wildcards added automatically)
await get(filter="title ~ 'Hello'")
# Null checks
await get(filter="deleted = null")
await get(filter="deleted != null")
# JSON/array fields
await get(filter="tags ~ ['python', 'vski']")
await get(filter="tags ~ 'python'")
page1 = await client.collection("posts").get_list(1, 20)
page2 = await client.collection("posts").get_list(2, 20)
if page1["page"] < page1["totalPages"]:
... # more pages available

When you have relation fields, you can expand them:

post = await client.collection("posts").get_one("post-id", expand="author,category")
print(post["expand"]["author"]) # full author record
print(post["expand"]["category"]) # full category record

Schema changes (add/remove fields, indexes, rules, FTS) belong in the YAML; vski apply diffs desired against live and emits the migration. To drop a collection, mark it state: absent (or delete it with vski collections rm, which removes the table immediately).

collections:
old_posts:
state: absent

Inspecting what exists:

Terminal window
vski collections list
vski collections show posts # fields, rules, indexes, views

Reading collection metadata from the SDK:

result = await client.settings.collections.get_list()
posts = next(c for c in result["items"] if c["name"] == "posts")
print(posts["schema"])

Warning: Deleting a collection deletes all its records.

  1. Use meaningful field names: follow consistent naming conventions
  2. Set required fields: use required: true for essential data
  3. Index frequently queried fields: improve query performance
  4. Use appropriate types: choose the most specific type for your data
  5. Validate on client side: reduce unnecessary API calls
  6. Use pagination: always use get_list() with limits for large datasets
MethodEndpointDescription
GET/api/collectionsList collections
POST/api/collectionsCreate collection
GET/api/collections/:nameGet collection
PATCH/api/collections/:nameUpdate collection
DELETE/api/collections/:nameDelete collection
GET/api/collections/:name/recordsList records
POST/api/collections/:name/recordsCreate record
GET/api/collections/:name/records/:recordIdGet record
PATCH/api/collections/:name/records/:recordIdUpdate record
PUT/api/collections/:name/records/:recordIdUpdate record (full)
DELETE/api/collections/:name/records/:recordIdDelete record
PATCH/api/collections/:name/records/bulkBulk update records
DELETE/api/collections/:name/records/bulkBulk delete records
GET/api/collections/:name/records/searchFull-text search records
GET/api/collections/:name/records/views/:slugGet records from view