This document describes the database schema for the Decentraland Events Service. The schema uses PostgreSQL and is managed through migrations located in src/migrations/.
erDiagram
events {
UUID id PK
TEXT name
TEXT image
TEXT description
TIMESTAMP start_at
TIMESTAMP finish_at
INTEGER[] coordinates
TEXT user
BOOLEAN approved
TIMESTAMP created_at
TIMESTAMP updated_at
TEXT contact
TEXT details
INTEGER total_attendees
TEXT[] latest_attendees
TEXT url
TEXT scene_name
TEXT user_name
BOOLEAN rejected
BOOLEAN trending
TEXT server
TEXT estate_id
TEXT estate_name
INTEGER x
INTEGER y
BOOLEAN all_day
BOOLEAN recurrent
TEXT recurrent_frequency
INTEGER recurrent_weekday_mask
INTEGER recurrent_month_mask
INTEGER recurrent_interval
INTEGER recurrent_count
TIMESTAMP recurrent_until
INTEGER duration
TIMESTAMP[] recurrent_dates
INTEGER recurrent_setpos
INTEGER recurrent_monthday
BOOLEAN highlighted
TIMESTAMP next_start_at
TIMESTAMP next_finish_at
TSVECTOR textsearch
VARCHAR[] categories
UUID[] schedules
TEXT approved_by
TEXT rejected_by
BOOLEAN world
TEXT place_id
TEXT community_id
}
event_attendees {
UUID event_id PK,FK
TEXT user PK
TIMESTAMP created_at
}
event_category {
VARCHAR name PK
BOOLEAN active
}
schedules {
UUID id PK
VARCHAR name
TEXT description
VARCHAR[] background
TEXT image
TIMESTAMPTZ active_since
TIMESTAMPTZ active_until
BOOLEAN active
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
VARCHAR theme
}
profile_settings {
TEXT user PK
TEXT email
BOOLEAN email_verified
BOOLEAN use_local_time
BOOLEAN notify_by_email
BOOLEAN notify_by_browser
TIMESTAMPTZ email_verified_at
TIMESTAMPTZ email_updated_at
TEXT[] permissions
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
profile_subscription {
TEXT endpoint PK
TEXT user
TEXT p256dh
TEXT auth
}
jobs {
UUID id PK
TEXT name
JSONB payload
TIMESTAMPTZ run_at
TIMESTAMPTZ created_at
}
events ||--o{ event_attendees : "has"
events }o--o{ event_category : "categorized by"
events }o--o{ schedules : "included in"
profile_settings ||--o{ profile_subscription : "has"
event_attendees }o--|| profile_settings : "belongs to"
The database contains the following main tables:
events- Core event information including location, dates, recurrence, and approval statusevent_attendees- Tracks user attendance for eventsevent_category- Defines available event categories/tagsschedules- Curated event collections with custom themingprofile_settings- User notification preferences and permissionsprofile_subscription- Web Push subscription data for browser notificationsjobs- Background job queue for async processing (managed by decentraland-gatsby)
Stores all event information including one-time and recurrent events with location data, approval status, and attendance tracking.
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
UUID | NOT NULL | Primary Key. Unique event identifier |
name |
TEXT | NOT NULL | Event name/title |
image |
TEXT | NULL | URL to event poster image stored in S3 |
description |
TEXT | NULL | Event description in markdown format |
start_at |
TIMESTAMP | NOT NULL | Event start time in UTC |
finish_at |
TIMESTAMP | NOT NULL | Event end time in UTC |
coordinates |
INTEGER[] | NOT NULL | Array of [x, y] coordinates in Decentraland Genesis City |
next_start_at |
TIMESTAMP | NOT NULL | Next occurrence start time for recurrent events |
next_finish_at |
TIMESTAMP | NOT NULL | Next occurrence end time for recurrent events |
duration |
INTEGER | NOT NULL | Event duration in milliseconds |
all_day |
BOOLEAN | NOT NULL | Whether event is all-day (default: false) |
x |
INTEGER | NOT NULL | X coordinate in Decentraland Genesis City (default: 0) |
y |
INTEGER | NOT NULL | Y coordinate in Decentraland Genesis City (default: 0) |
server |
TEXT | NULL | Deprecated. Server/realm name (use world field instead) |
url |
TEXT | NULL | Custom event URL or external link |
scene_name |
TEXT | NULL | Name of the scene where the event takes place |
user |
TEXT | NOT NULL | Creator wallet address. Stored in lowercase |
user_name |
TEXT | NULL | Creator display name |
estate_id |
TEXT | NULL | Associated Decentraland estate ID |
estate_name |
TEXT | NULL | Estate name for display |
approved |
BOOLEAN | NOT NULL | Approval status by moderator (default: false) |
rejected |
BOOLEAN | NOT NULL | Rejection status by moderator (default: false) |
approved_by |
TEXT | NULL | Moderator wallet address who approved. Stored in lowercase |
rejected_by |
TEXT | NULL | Moderator wallet address who rejected. Stored in lowercase |
highlighted |
BOOLEAN | NOT NULL | Featured/highlighted status (default: false) |
trending |
BOOLEAN | NOT NULL | Trending status calculated algorithmically (default: false) |
recurrent |
BOOLEAN | NOT NULL | Whether event is recurrent (default: false) |
recurrent_frequency |
TEXT | NULL | RRule frequency: YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY |
recurrent_setpos |
INTEGER | NULL | Position in recurrence pattern (e.g., 1 for first, -1 for last) |
recurrent_monthday |
INTEGER | NULL | Day of month for monthly recurrence (1-31) |
recurrent_weekday_mask |
INTEGER | NOT NULL | Bitmask for weekdays (default: 0) |
recurrent_month_mask |
INTEGER | NOT NULL | Bitmask for months (default: 0) |
recurrent_interval |
INTEGER | NOT NULL | Interval between occurrences (default: 1) |
recurrent_count |
INTEGER | NULL | Total number of occurrences |
recurrent_until |
TIMESTAMP | NULL | Recurrence end date |
recurrent_dates |
TIMESTAMP[] | NOT NULL | Pre-calculated array of occurrence dates (default: {}) |
contact |
TEXT | NULL | Contact information for event organizer |
details |
TEXT | NULL | Additional event details or notes |
total_attendees |
INTEGER | NOT NULL | Denormalized count of attendees (default: 0) |
latest_attendees |
TEXT[] | NOT NULL | Array of recent attendee addresses (default: {}) |
textsearch |
TSVECTOR | NULL | Full-text search vector for name and description |
categories |
VARCHAR(50)[] | NOT NULL | Array of event category tags (default: {}) |
schedules |
UUID[] | NOT NULL | Array of associated schedule IDs (default: {}) |
world |
BOOLEAN | NOT NULL | Whether event is in a virtual world vs land (default: false) |
place_id |
TEXT | NULL | Associated place ID from Catalyst |
community_id |
TEXT | NULL | Associated community ID from Communities API |
created_at |
TIMESTAMP | NOT NULL | Creation timestamp (default: now()) |
updated_at |
TIMESTAMP | NOT NULL | Last update timestamp (default: now()) |
- Primary Key:
id idx_events_coordinateson(x, y)columns for location-based queriesidx_events_start_atonstart_atcolumn for date filteringidx_events_next_start_atonnext_start_atcolumn for recurrent event queriesidx_events_approvedonapprovedcolumn for filtering approved eventsidx_events_rejectedonrejectedcolumn for filtering rejected eventsidx_events_textsearch(GIN index) ontextsearchcolumn for full-text searchidx_events_categories(GIN index) oncategoriesarray for category filteringidx_events_sitemapcomposite index for SEO sitemap generation
- Primary Key uniqueness: The
idcolumn must be unique - Date validity:
finish_atmust be greater thanstart_at - Recurrence validity: If
recurrentis true,recurrent_frequencymust not be null - Approval exclusivity: An event cannot be both approved and rejected (
NOT (approved AND rejected)) - Coordinate range: Valid Decentraland coordinates typically range from -150 to 150
- Address normalization: Wallet addresses in
user,approved_by, andrejected_byfields must be stored in lowercase - Attendee limit: The
latest_attendeesarray is capped at a configurable limit for performance - Default coordinates: Coordinates (0,0) represent Decentraland's Genesis Plaza
- Textsearch updates: The
textsearchcolumn is automatically updated via trigger whennameordescriptionchanges - Total attendees sync: The
total_attendeescount must match the number of records inevent_attendeesfor that event - Next occurrence calculation: For recurrent events,
next_start_atandnext_finish_atare pre-calculated for query performance
- RRule implementation: Recurrence follows RFC 5545 (iCalendar) specification
- Image storage: Event images are stored in AWS S3 and the
imagefield contains the public URL - Categories limit: Currently limited to 1 category per event (MAX_CATEGORIES_ALLOWED = 1)
Tracks user attendance for events with composite primary key to ensure one attendance record per user per event.
| Column | Type | Nullable | Description |
|---|---|---|---|
event_id |
UUID | NOT NULL | Primary Key 1, Foreign Key. References events(id) |
user |
TEXT | NOT NULL | Primary Key 2. Attendee wallet address. Stored in lowercase |
user_name |
TEXT | NULL | Attendee display name |
created_at |
TIMESTAMP | NOT NULL | Attendance timestamp (default: now()) |
- Composite Primary Key:
(event_id, user) idx_event_attendees_eventonevent_idfor event attendee list queriesidx_event_attendees_useronuserfor user attendance history queries
- Foreign Key:
event_idreferencesevents(id)ON DELETE CASCADE - Composite uniqueness: The combination of
(event_id, user)must be unique
- Address normalization: Wallet addresses in
userfield must be stored in lowercase - Attendee count sync: When an attendance record is created or deleted,
events.total_attendeesmust be updated - Latest attendees update: When attendance is created, the user should be added to
events.latest_attendeesarray
- Cascade deletion: When an event is deleted, all attendance records are automatically deleted
Defines available event categories/tags that can be assigned to events for filtering and discovery.
| Column | Type | Nullable | Description |
|---|---|---|---|
name |
VARCHAR(50) | NOT NULL | Primary Key. Category identifier (e.g., "music", "art", "gaming") |
active |
BOOLEAN | NOT NULL | Whether category is active and visible in UI (default: false) |
created_at |
TIMESTAMPTZ | NOT NULL | Creation timestamp (default: now()) |
updated_at |
TIMESTAMPTZ | NOT NULL | Last update timestamp (default: now()) |
- Primary Key:
name idx_event_category_activeonactivecolumn for filtering active categories
- Primary Key uniqueness: The
namecolumn must be unique - Name length: Category name must not exceed 50 characters
- Active visibility: Only active categories are displayed in the event creation UI
- Category limit: Events can have multiple categories but currently limited to 1 (MAX_CATEGORIES_ALLOWED = 1)
- Available categories: art, causes, competition, education, gambling, gaming, giveaway, health, hobbies, identity, live, music, networking, nft, other, party, play, poap, religion, shopping, social, sports, talks, town, tv
Curated collections of events with custom theming used for special occasions like festivals, fashion weeks, or themed event series.
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
UUID | NOT NULL | Primary Key. Schedule identifier |
name |
VARCHAR(50) | NOT NULL | Schedule name (e.g., "Metaverse Festival 2022") |
description |
VARCHAR(255) | NULL | Schedule description |
image |
VARCHAR(255) | NULL | Schedule image URL for display |
theme |
VARCHAR(30) | NULL | Theme identifier (e.g., "mvmf_2022", "mvfw_2023", "pride_2023") |
background |
VARCHAR(30)[] | NOT NULL | Array of RGBA color strings for gradient background |
active |
BOOLEAN | NOT NULL | Whether schedule is currently active (default: false) |
active_since |
TIMESTAMPTZ | NOT NULL | Schedule activation start date (default: now()) |
active_until |
TIMESTAMPTZ | NOT NULL | Schedule activation end date (default: now()) |
created_at |
TIMESTAMPTZ | NOT NULL | Creation timestamp (default: now()) |
updated_at |
TIMESTAMPTZ | NOT NULL | Last update timestamp (default: now()) |
- Primary Key:
id idx_schedules_activeonactivecolumn for filtering active schedulesidx_schedules_dateson(active_since, active_until)for date-based queries
- Primary Key uniqueness: The
idcolumn must be unique - Date validity:
active_untilmust be greater thanactive_since - Name length: Schedule name must not exceed 50 characters
- Time-based activation: Schedules are automatically activated/deactivated based on
active_sinceandactive_untildates - Background format: Background array contains RGBA color strings for creating gradient effects in UI
- Many-to-many relationship: Events reference schedules via the
events.schedulesUUID array - Theme customization: Theme identifier determines custom styling and branding in the frontend
Stores user preferences and notification settings with one record per user wallet address.
| Column | Type | Nullable | Description |
|---|---|---|---|
user |
TEXT | NOT NULL | Primary Key. User wallet address. Stored in lowercase |
email |
TEXT | NULL | User email address for notifications |
email_verified |
BOOLEAN | NOT NULL | Email verification status (default: false) |
use_local_time |
BOOLEAN | NOT NULL | Display event times in local timezone (default: false) |
notify_by_email |
BOOLEAN | NOT NULL | Email notification preference (default: false) |
notify_by_browser |
BOOLEAN | NOT NULL | Browser push notification preference (default: false) |
email_verified_at |
TIMESTAMPTZ | NULL | Timestamp when email was verified |
email_updated_at |
TIMESTAMPTZ | NULL | Timestamp when email was last updated |
permissions |
TEXT[] | NOT NULL | Array of user permissions for admin/moderator access (default: {}) |
created_at |
TIMESTAMPTZ | NOT NULL | Profile creation timestamp (default: now()) |
updated_at |
TIMESTAMPTZ | NOT NULL | Last update timestamp (default: now()) |
- Primary Key:
user idx_profile_settings_emailonemailcolumn for email lookup queriesidx_profile_settings_permissions(GIN index) onpermissionsarray for authorization checks
- Primary Key uniqueness: The
usercolumn must be unique - Email format: Email must be valid format when not null
- Address normalization: Wallet addresses in
userfield must be stored in lowercase - Email verification required: Email notifications only sent if
email_verifiedis true - Browser notification dependency: If
notify_by_browseris true, user must have at least one record inprofile_subscription
- Permissions array: Controls access to admin/moderator features like event approval/rejection
- One profile per user: Each wallet address has exactly one profile settings record
Stores Web Push subscription data for browser notifications using VAPID protocol.
| Column | Type | Nullable | Description |
|---|---|---|---|
endpoint |
TEXT | NOT NULL | Primary Key. Push service endpoint URL provided by browser |
user |
TEXT | NOT NULL | User wallet address. Stored in lowercase |
p256dh |
TEXT | NOT NULL | Public key (P-256 ECDH) for message encryption |
auth |
TEXT | NOT NULL | Authentication secret for push subscription |
- Primary Key:
endpoint idx_profile_subscription_useronusercolumn for user subscription lookups
- Primary Key uniqueness: The
endpointcolumn must be unique (one subscription per endpoint)
- Address normalization: Wallet addresses in
userfield must be stored in lowercase - Multiple subscriptions: One user can have multiple subscriptions for different devices/browsers
- Browser permission required: User must grant notification permission in browser before subscription is created
- VAPID protocol: Uses Voluntary Application Server Identification (VAPID) for Web Push
- Related setting: Connected to
profile_settings.notify_by_browserpreference - Endpoint format: Endpoint URL is provided by browser's Push API and uniquely identifies the subscription
Background job queue for async processing managed by the decentraland-gatsby framework.
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
UUID | NOT NULL | Primary Key. Job identifier |
name |
TEXT | NOT NULL | Job name/type identifier (e.g., "notifyUpcomingEvents") |
payload |
JSONB | NULL | Job input data and parameters |
run_at |
TIMESTAMPTZ | NOT NULL | Scheduled time for job execution |
created_at |
TIMESTAMPTZ | NOT NULL | Job creation timestamp (default: now()) |
- Primary Key:
id idx_jobs_run_atonrun_atcolumn for scheduling queriesidx_jobs_nameonnamecolumn for filtering by job type
- Primary Key uniqueness: The
idcolumn must be unique
- Scheduled execution: Jobs are executed when
run_attime is reached - Non-blocking operations: Jobs are processed asynchronously to avoid blocking HTTP requests
- Common job names: notifyUpcomingEvents, notifyStartedEvents, notifyEndedEvents, updateNextStartAt
- Framework managed: Managed by decentraland-gatsby framework with background workers
- JSONB flexibility:
payloadfield uses JSONB for flexible schema-less storage