app.zeens.event

zeens.app

Documentation

An event: a photowalk, workshop, meetup, exhibition or talk. Written to the organiser's own PDS. Others RSVP via app.zeens.rsvp (ATProto handle) or via an email address tracked off-protocol in PocketBase. The app does NOT collect payment: paid events carry free-text paymentInstructions and any URL/link in that field is rejected at write time. Carries the app.zeens.v marker so firehose + Lua filters recognise it. NOTE: the record id stays 'app.zeens.event' and the app.zeens.* namespace is network plumbing — it is not renamed when the product is. Two orthogonal axes describe an event: `mode` (WHERE it happens — in person, online, or both) and `categories` (WHAT it is — one or more of photowalk/workshop/meetup/exhibition/talk/other). The `hostedBy` field is DEPRECATED (the organiser is the record owner); kept for back-compat with old records.

main record

An event: a photowalk, workshop, meetup, exhibition or talk. Written to the organiser's own PDS. Others RSVP via app.zeens.rsvp (ATProto handle) or via an email address tracked off-protocol in PocketBase. The app does NOT collect payment: paid events carry free-text paymentInstructions and any URL/link in that field is rejected at write time. Carries the app.zeens.v marker so firehose + Lua filters recognise it. NOTE: the record id stays 'app.zeens.event' and the app.zeens.* namespace is network plumbing — it is not renamed when the product is. Two orthogonal axes describe an event: `mode` (WHERE it happens — in person, online, or both) and `categories` (WHAT it is — one or more of photowalk/workshop/meetup/exhibition/talk/other). The `hostedBy` field is DEPRECATED (the organiser is the record owner); kept for back-compat with old records.

Record Key tid Timestamp-based ID

Properties

app.zeens.v integer Required

Zeens version marker (always 1).

approvalRequired boolean Optional

When true, an RSVP is pending until the organiser approves it; only approved RSVPs count toward the cap and the attendee list. Approval state is tracked off-protocol (the organiser cannot mutate another user's rsvp record). Default false.

capacityMode string Optional

Behaviour when maxParticipants is reached. 'hard' = block new going RSVPs. 'waitlist' = overflow goes on a waitlist, promoted in RSVP order. Only meaningful when maxParticipants is set. Default 'hard'.

Known values: hard, waitlist
categories array of string Required

WHAT the event is. One or more (max 3) — an event can be both a photowalk and a meetup. Drives browse/filter, which OR-matches any element. Orthogonal to `mode`. The legacy value 'online_meetup' is tolerated on read and means mode 'online' + category 'meetup'.

maxLength: 3 itemsminLength: 1 items
coverImage blob Optional

Wide cover image shown on the event header and share previews.

maxSize: 4.0 MB
createdAt string datetime Required

An RFC 3339 formatted timestamp.

createdBy string Optional

Display text for the event creator/organiser (the account that owns this record).

maxLength: 200 bytes
description string Optional

Long-form event description.

maxLength: 3000 bytes
endsAt string datetime Optional

Optional event end instant.

facets array of ref app.bsky.richtext.facet Optional

Rich-text facets (links, mentions) over the description, reusing the Bluesky richtext lexicon.

hostedBy string Optional

DEPRECATED. Display text for who is hosting. The organiser is the record owner, so this is no longer written; kept so old records still read.

maxLength: 200 bytes
itinerary array of ref #itineraryItem Optional

Ordered agenda for the event: a list of items, each with optional time, descriptive text, and an optional location. Works for any event type — a photowalk's route, a workshop's schedule, an exhibition's programme. Items that carry coordinates are drawn on the event map as numbered pins in order; an event whose items have no coordinates simply renders the list with no map.

maxLength: 30 items
location ref #location Optional

Physical location. Expected for mode 'in_person' and 'hybrid'; absent for 'online'.

maxParticipants integer Optional

Optional cap on confirmed (going) participants. Absent = unlimited.

minimum: 1
mode string Required

WHERE the event happens, and one of exactly three values. 'in_person' needs a location; 'online' needs a virtualUrl and has no location; 'hybrid' needs both. Orthogonal to `categories` — an online workshop is mode 'online' + categories ['workshop']. Records written before this field carried meaning default to 'in_person' on read. The legacy value 'virtual' is tolerated on read and means 'online'.

Known values: in_person, online, hybrid
paid boolean Optional

Whether the event costs money. The app never processes payment.

paymentInstructions string Optional

Free-text explanation of how the organiser collects payment out of band. MUST NOT contain a URL/link; rejected at write time client- and server-side.

maxLength: 1000 bytes
photoCategories array of string Optional

Photography genre tags (Street, Landscape, Portrait, ...). Up to 3, optional. Drawn from the shared category list used by posts. Distinct from `categories`, which is the event type.

maxLength: 3 items
rsvpPolicy string Required

Who may RSVP. 'atmosphere' = any ATProto account. 'photosky' = members only. 'anyone_email' = anyone, including non-ATProto people via an email address. Enforcement is soft (obscurity-level), like collection visibility.

Known values: atmosphere, photosky, anyone_email
startsAt string datetime Required

Event start, as an absolute instant. Render in `timezone`.

timezone string Required

IANA timezone name (e.g. 'America/New_York'). Stored separately from startsAt so calendar export (.ics DTSTART;TZID) and the displayed local time are correct.

maxLength: 100 bytes
title string Required

Event title.

maxLength: 300 bytesmaxGraphemes: 150 graphemes
virtualUrl string uri Optional

Join link for events with mode 'online' or 'hybrid'. Required for those modes. Shown only to RSVP'd users in the client.

View raw schema
{
  "description": "An event: a photowalk, workshop, meetup, exhibition or talk. Written to the organiser's own PDS. Others RSVP via app.zeens.rsvp (ATProto handle) or via an email address tracked off-protocol in PocketBase. The app does NOT collect payment: paid events carry free-text paymentInstructions and any URL/link in that field is rejected at write time. Carries the app.zeens.v marker so firehose + Lua filters recognise it. NOTE: the record id stays 'app.zeens.event' and the app.zeens.* namespace is network plumbing — it is not renamed when the product is. Two orthogonal axes describe an event: `mode` (WHERE it happens — in person, online, or both) and `categories` (WHAT it is — one or more of photowalk/workshop/meetup/exhibition/talk/other). The `hostedBy` field is DEPRECATED (the organiser is the record owner); kept for back-compat with old records.",
  "key": "tid",
  "record": {
    "properties": {
      "app.zeens.v": {
        "description": "Zeens version marker (always 1).",
        "type": "integer"
      },
      "approvalRequired": {
        "description": "When true, an RSVP is pending until the organiser approves it; only approved RSVPs count toward the cap and the attendee list. Approval state is tracked off-protocol (the organiser cannot mutate another user's rsvp record). Default false.",
        "type": "boolean"
      },
      "capacityMode": {
        "description": "Behaviour when maxParticipants is reached. 'hard' = block new going RSVPs. 'waitlist' = overflow goes on a waitlist, promoted in RSVP order. Only meaningful when maxParticipants is set. Default 'hard'.",
        "knownValues": [
          "hard",
          "waitlist"
        ],
        "type": "string"
      },
      "categories": {
        "description": "WHAT the event is. One or more (max 3) — an event can be both a photowalk and a meetup. Drives browse/filter, which OR-matches any element. Orthogonal to `mode`. The legacy value 'online_meetup' is tolerated on read and means mode 'online' + category 'meetup'.",
        "items": {
          "knownValues": [
            "photowalk",
            "workshop",
            "meetup",
            "exhibition",
            "talk",
            "other"
          ],
          "type": "string"
        },
        "maxLength": 3,
        "minLength": 1,
        "type": "array"
      },
      "coverImage": {
        "accept": [
          "image/jpeg",
          "image/png",
          "image/webp"
        ],
        "description": "Wide cover image shown on the event header and share previews.",
        "maxSize": 4000000,
        "type": "blob"
      },
      "createdAt": {
        "format": "datetime",
        "type": "string"
      },
      "createdBy": {
        "description": "Display text for the event creator/organiser (the account that owns this record).",
        "maxLength": 200,
        "type": "string"
      },
      "description": {
        "description": "Long-form event description.",
        "maxLength": 3000,
        "type": "string"
      },
      "endsAt": {
        "description": "Optional event end instant.",
        "format": "datetime",
        "type": "string"
      },
      "facets": {
        "description": "Rich-text facets (links, mentions) over the description, reusing the Bluesky richtext lexicon.",
        "items": {
          "ref": "app.bsky.richtext.facet",
          "type": "ref"
        },
        "type": "array"
      },
      "hostedBy": {
        "description": "DEPRECATED. Display text for who is hosting. The organiser is the record owner, so this is no longer written; kept so old records still read.",
        "maxLength": 200,
        "type": "string"
      },
      "itinerary": {
        "description": "Ordered agenda for the event: a list of items, each with optional time, descriptive text, and an optional location. Works for any event type — a photowalk's route, a workshop's schedule, an exhibition's programme. Items that carry coordinates are drawn on the event map as numbered pins in order; an event whose items have no coordinates simply renders the list with no map.",
        "items": {
          "ref": "#itineraryItem",
          "type": "ref"
        },
        "maxLength": 30,
        "type": "array"
      },
      "links": {
        "description": "Social / website links for the event.",
        "items": {
          "ref": "#link",
          "type": "ref"
        },
        "maxLength": 5,
        "type": "array"
      },
      "location": {
        "description": "Physical location. Expected for mode 'in_person' and 'hybrid'; absent for 'online'.",
        "ref": "#location",
        "type": "ref"
      },
      "maxParticipants": {
        "description": "Optional cap on confirmed (going) participants. Absent = unlimited.",
        "minimum": 1,
        "type": "integer"
      },
      "mode": {
        "description": "WHERE the event happens, and one of exactly three values. 'in_person' needs a location; 'online' needs a virtualUrl and has no location; 'hybrid' needs both. Orthogonal to `categories` — an online workshop is mode 'online' + categories ['workshop']. Records written before this field carried meaning default to 'in_person' on read. The legacy value 'virtual' is tolerated on read and means 'online'.",
        "knownValues": [
          "in_person",
          "online",
          "hybrid"
        ],
        "type": "string"
      },
      "paid": {
        "description": "Whether the event costs money. The app never processes payment.",
        "type": "boolean"
      },
      "paymentInstructions": {
        "description": "Free-text explanation of how the organiser collects payment out of band. MUST NOT contain a URL/link; rejected at write time client- and server-side.",
        "maxLength": 1000,
        "type": "string"
      },
      "photoCategories": {
        "description": "Photography genre tags (Street, Landscape, Portrait, ...). Up to 3, optional. Drawn from the shared category list used by posts. Distinct from `categories`, which is the event type.",
        "items": {
          "maxLength": 100,
          "type": "string"
        },
        "maxLength": 3,
        "type": "array"
      },
      "rsvpPolicy": {
        "description": "Who may RSVP. 'atmosphere' = any ATProto account. 'photosky' = members only. 'anyone_email' = anyone, including non-ATProto people via an email address. Enforcement is soft (obscurity-level), like collection visibility.",
        "knownValues": [
          "atmosphere",
          "photosky",
          "anyone_email"
        ],
        "type": "string"
      },
      "startsAt": {
        "description": "Event start, as an absolute instant. Render in `timezone`.",
        "format": "datetime",
        "type": "string"
      },
      "timezone": {
        "description": "IANA timezone name (e.g. 'America/New_York'). Stored separately from startsAt so calendar export (.ics DTSTART;TZID) and the displayed local time are correct.",
        "maxLength": 100,
        "type": "string"
      },
      "title": {
        "description": "Event title.",
        "maxGraphemes": 150,
        "maxLength": 300,
        "type": "string"
      },
      "virtualUrl": {
        "description": "Join link for events with mode 'online' or 'hybrid'. Required for those modes. Shown only to RSVP'd users in the client.",
        "format": "uri",
        "type": "string"
      }
    },
    "required": [
      "title",
      "startsAt",
      "timezone",
      "mode",
      "rsvpPolicy",
      "categories",
      "createdAt",
      "app.zeens.v"
    ],
    "type": "object"
  },
  "type": "record"
}
itineraryItem object

No description available.

Properties

location ref #location Optional

Optional location for this item. Only meaningful for events with mode 'in_person' or 'hybrid'. Items with lat/lng are plotted on the event map.

text string Required

What happens at this point in the event (e.g. 'Meet at the park entrance', 'Darkroom demo', 'Q&A with the curator').

maxLength: 500 bytes
time string Optional

Free-text time label for this item (e.g. '9:00 AM', 'Lunch'). Stored as text, not a datetime, so it renders verbatim without timezone handling.

maxLength: 50 bytes
View raw schema
{
  "properties": {
    "location": {
      "description": "Optional location for this item. Only meaningful for events with mode 'in_person' or 'hybrid'. Items with lat/lng are plotted on the event map.",
      "ref": "#location",
      "type": "ref"
    },
    "text": {
      "description": "What happens at this point in the event (e.g. 'Meet at the park entrance', 'Darkroom demo', 'Q&A with the curator').",
      "maxLength": 500,
      "type": "string"
    },
    "time": {
      "description": "Free-text time label for this item (e.g. '9:00 AM', 'Lunch'). Stored as text, not a datetime, so it renders verbatim without timezone handling.",
      "maxLength": 50,
      "type": "string"
    }
  },
  "required": [
    "text"
  ],
  "type": "object"
}
link object

No description available.

Properties

label string Optional

No description available.

maxLength: 100 bytes
uri string uri Required

A valid URI.

View raw schema
{
  "properties": {
    "label": {
      "maxLength": 100,
      "type": "string"
    },
    "uri": {
      "format": "uri",
      "type": "string"
    }
  },
  "required": [
    "uri"
  ],
  "type": "object"
}
location object

No description available.

Properties

address string Optional

Human-readable street address.

maxLength: 500 bytes
lat integer Optional

Latitude in microdegrees (degrees x 1e7). ATProto rejects non-integer numbers, so coordinates are scaled to integers.

lng integer Optional

Longitude in microdegrees (degrees x 1e7).

name string Optional

Venue / place name.

maxLength: 200 bytes
View raw schema
{
  "properties": {
    "address": {
      "description": "Human-readable street address.",
      "maxLength": 500,
      "type": "string"
    },
    "lat": {
      "description": "Latitude in microdegrees (degrees x 1e7). ATProto rejects non-integer numbers, so coordinates are scaled to integers.",
      "type": "integer"
    },
    "lng": {
      "description": "Longitude in microdegrees (degrees x 1e7).",
      "type": "integer"
    },
    "name": {
      "description": "Venue / place name.",
      "maxLength": 200,
      "type": "string"
    }
  },
  "type": "object"
}

Lexicon Garden

@