supabase
Handles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...
meteor-sync-specialist
Sync data from Meteor MongoDB to Supabase; use when running syncs, debugging sync failures, investigating entity mapping issues, or implementing new mappers
Full skill instructions
Use this skill when:
meteor_entity_mapping tableDO NOT use for:
mongodb-production-query skill)airtable-sync-specialist skill)database-migration-manager skill)# MongoDB connection (Meteor production database)
export METEOR_MONGO_URL="mongodb://root:<password>@mdb-p-0.ballee.db-eu2.zcloud.ws:60601,mdb-p-1.ballee.db-eu2.zcloud.ws:60602,mdb-p-2.ballee.db-eu2.zcloud.ws:60603/meteor?authSource=admin&ssl=true&tlsInsecure=true&replicaSet=mdb-p"
# Supabase staging database
export SUPABASE_DB_PASSWORD_STAGING="<password>"
export SUPABASE_DB_HOST="db.hxpcknyqswetsqmqmeep.supabase.co" # Optional, has default
# Supabase production database (use with caution)
export SUPABASE_DB_PASSWORD_PROD="<password>"
const S3_REGION = 'eu-central-1';
const S3_BUCKET = 'ch.akson.dance.bucket';
// URL format for media files
const mediaUrl = `https://s3.${S3_REGION}.amazonaws.com/${S3_BUCKET}/${s3Key}`;
// R2 URLs follow this pattern
const r2Url = 'https://pub-2541aa5053334bd4bcd4a619e7ad3428.r2.dev/...';
| Entity Type | Count | Notes |
|---|---|---|
| auth.users | 14,864 | Migrated from MongoDB users |
| profiles | 14,864 | Auto-created by trigger |
| profiles with meteor_id | 14,819 | Linked to MongoDB |
| professional_profiles with user_id | 14,603 | Linked to auth users |
| dancer_media | 47,156 | photos: 37,822, headers: 7,251, videos: 2,079 |
| organization_media | 3,809 | Logos and headers |
| profile_media | 2,283 | User-uploaded media (orphan media) |
| post_media | 1,631 | Media attached to posts |
| profile_posts | 1,491 | Community posts |
| community_events | 293 | Community events |
All media URLs have been verified functional (HTTP 200):
https://s3.eu-central-1.amazonaws.com/ch.akson.dance.bucket/...https://pub-2541aa5053334bd4bcd4a619e7ad3428.r2.dev/...https://www.youtube.com/embed/...MeteorSyncService (meteor-sync.service.ts)
MeteorDuplicatePreventionService (meteor-duplicate-prevention.service.ts)
_idMeteorChangeDetectorService (meteor-change-detector.service.ts)
Entity Mappers (mappers/)
Media Transfer Service (media-transfer.service.ts)
-- Entity mapping cache (deduplication)
CREATE TABLE meteor_entity_mapping (
meteor_id text PRIMARY KEY,
entity_type text NOT NULL,
entity_id uuid NOT NULL,
source_hash text, -- MD5 hash for change detection
source_data jsonb, -- Cached MongoDB document
first_synced_at timestamptz,
last_synced_at timestamptz,
sync_count integer DEFAULT 1,
sync_status text DEFAULT 'synced', -- synced | pending | error | skipped
error_message text
);
-- Sync run tracking
CREATE TABLE meteor_sync_runs (
id uuid PRIMARY KEY,
sync_type text NOT NULL, -- full | incremental | entity | resume
status text NOT NULL, -- running | completed | failed | partial
trigger_type text, -- manual | cron | script | webhook
triggered_by uuid,
started_at timestamptz,
completed_at timestamptz,
duration_seconds integer,
metrics jsonb -- created, updated, skipped, failed counts
);
-- Change tracking
CREATE TABLE meteor_sync_changes (
id uuid PRIMARY KEY,
sync_run_id uuid REFERENCES meteor_sync_runs(id),
meteor_id text,
entity_type text,
operation text, -- created | updated | skipped | error
changed_fields jsonb, -- Array of field changes
error_message text,
created_at timestamptz
);
-- Sync state (for incremental sync)
CREATE TABLE meteor_sync_state (
entity_type text PRIMARY KEY,
last_sync_at timestamptz,
last_meteor_id text, -- For resume capability
checkpoint_data jsonb
);
# Full sync of all entities
pnpm meteor:sync --full
# Incremental sync since last run
pnpm meteor:sync --incremental
# Sync specific entity type
pnpm meteor:sync --entity organization
pnpm meteor:sync --entity candidate
pnpm meteor:sync --entity media
# Preview changes without applying
pnpm meteor:sync --dry-run
# Resume a failed sync
pnpm meteor:sync --resume <run-id>
Requirements:
METEOR_MONGO_URL - MongoDB connection stringSUPABASE_URL and SUPABASE_SERVICE_KEY environment variables# Check sync status
pnpm meteor:staging status
# Trigger a full sync
pnpm meteor:staging trigger
# Trigger incremental sync
pnpm meteor:staging trigger --type incremental
# Sync specific entity type
pnpm meteor:staging trigger --entity organization
# Dry run (preview only)
pnpm meteor:staging trigger --dry-run
# View sync history
pnpm meteor:staging history
pnpm meteor:staging history --limit 20
# View detailed logs for a specific run
pnpm meteor:staging logs <run-id>
# View sync statistics
pnpm meteor:staging stats
# Watch sync status in real-time
pnpm meteor:staging watch
pnpm meteor:staging watch --interval 3
Requirements:
METEOR_SYNC_API_KEY - API key for authenticationSTAGING_URL (optional) - defaults to https://staging.ballee.appMigrates MongoDB users to Supabase auth.users with bcrypt password hash preservation:
# From skill scripts folder
cd .claude/skills/meteor-sync-specialist/scripts
# Preview migration (no changes)
npx tsx migrate-users.ts --dry-run
# Run full migration
npx tsx migrate-users.ts
What it does:
on_auth_user_created triggermeteor_id for linkingprofessional_profiles.user_id via preferredUserCandidateIdImportant: Before running, ensure trigger functions use public. schema prefix (see Troubleshooting section).
Syncs media that only has userId (no candidateId/organizationId) to profile_media:
# Preview sync
npx tsx sync-orphan-media.ts --dry-run
# Run sync
npx tsx sync-orphan-media.ts
Prerequisites: Users must be migrated first (migrate-users.ts)
Syncs media references from MongoDB Posts to post_media table:
# Preview sync
npx tsx sync-post-media.ts --dry-run
# Run sync
npx tsx sync-post-media.ts
Prerequisites: Posts must be synced first (profile_posts populated)
| Mapper | MongoDB Collection | Supabase Table(s) | Status |
|---|---|---|---|
OrganizationMapper | Organizations | organizations | Active |
OrganizationMediaMapper | Media | organization_media | Active |
CandidateMapper | Candidates | profiles + professional_profiles + dancer_profiles | Active |
DancerMediaMapper | Media | dancer_media | Active |
ExperienceMapper | Experiences | experiences | Active |
PostMapper | Posts | posts | Active |
LikeMapper | Likes | likes | Active |
FollowMapper | Follows | follows | Active |
AuditionMapper | VacanciesCollection | auditions | Planned |
AuditionApplicationMapper | Applications | audition_applications | Planned |
type MeteorEntityType =
| 'organization'
| 'organization_media'
| 'candidate'
| 'media'
| 'experience'
| 'post'
| 'follow'
| 'like'
| 'application'
| 'offer'
| 'user'
| 'audition'
| 'audition_application';
Entities must be synced in this order to resolve foreign keys:
Level 1 (No dependencies):
├── organization
├── organization_media
├── candidate
└── media (dancer_media)
Level 2 (Depends on Level 1):
├── experience (refs: organization, candidate)
├── audition (refs: organization)
└── post (refs: candidate, organization)
Level 3 (Depends on Level 2):
├── audition_application (refs: audition, candidate)
├── like (refs: post, candidate)
├── follow (refs: candidate, organization)
└── comment (refs: post, candidate)
| MongoDB Field | Supabase Field | Transform |
|---|---|---|
_id | meteor_id | Store for deduplication |
meta.createdAt | created_at | ISO timestamp |
meta.modifiedAt | updated_at | ISO timestamp |
meta.createdBy | created_by | Resolve to user_id |
| camelCase | snake_case | Field name transform |
| ObjectId refs | UUID | Resolve via entity_mapping |
The candidate mapper splits MongoDB documents across 3 Supabase tables:
// MongoDB Candidate document
{
_id: ObjectId,
userId: ObjectId,
firstName: string,
lastName: string,
stageName: string,
birthdate: Date,
bio: string,
danceStyles: string[],
height: number,
weight: number,
availableForHire: boolean,
// ... 30+ more fields
}
// Supabase Tables:
// 1. profiles (basic user info)
{
id: uuid,
display_name: string,
first_name: string,
last_name: string,
birthdate: date
}
// 2. professional_profiles (career info)
{
id: uuid,
user_id: uuid,
stage_name: string,
bio: text,
dance_styles: text[],
available_for_hire: boolean,
social_handles: jsonb
}
// 3. dancer_profiles (physical attributes)
{
id: uuid,
professional_profile_id: uuid,
height_cm: integer,
weight_kg: integer,
pointe_work: boolean,
acrobatics: boolean
}
// MongoDB → Supabase
{
_id: ObjectId, // → meteor_id
name: string, // → name
type: 'school' | 'company' | 'agency', // → type
logoMediaId: ObjectId, // → logo_media_id (resolve via mapping)
headerMediaId: ObjectId, // → header_media_id
bio: string, // → description
website: string, // → website
location: { // → address, city, country
city: string,
country: string
}
}
Syncs all documents from MongoDB regardless of previous sync state:
pnpm meteor:sync --full
Only syncs documents modified since last successful sync:
pnpm meteor:sync --incremental
meta.modifiedAt > last sync timestampmeteor_sync_state to track last syncSyncs specific entity types only:
pnpm meteor:sync --entity organization
pnpm meteor:sync --entity candidate
Continues a failed sync from the last checkpoint:
pnpm meteor:sync --resume <run-id>
meteor_sync_stateSymptoms:
relation "profiles" does not existrelation "professional_profiles" does not existCause:
Trigger functions on auth.users reference tables without public. schema prefix.
When running outside the normal application context, PostgreSQL can't find the tables.
Solution: Fix the trigger functions to use fully qualified table names:
-- Fix trigger_generate_profile_slug
CREATE OR REPLACE FUNCTION public.trigger_generate_profile_slug()
RETURNS TRIGGER AS $$
DECLARE
v_base_slug TEXT;
v_slug TEXT;
v_counter INTEGER := 0;
v_user_row RECORD;
BEGIN
-- Get user info - USE public.profiles instead of profiles
SELECT first_name, last_name, display_name INTO v_user_row
FROM public.profiles WHERE id = NEW.user_id;
-- ... rest of function
-- Replace all 'profiles' with 'public.profiles'
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Fix create_dancer_profile_on_signup
CREATE OR REPLACE FUNCTION public.create_dancer_profile_on_signup()
RETURNS TRIGGER AS $$
BEGIN
-- Use public.professional_profiles and public.dancer_profiles
INSERT INTO public.professional_profiles (id, user_id, profile_type, ...)
VALUES (gen_random_uuid(), NEW.id, 'dancer', ...);
INSERT INTO public.dancer_profiles (id, ...)
VALUES (...);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Fix setup_new_user
CREATE OR REPLACE FUNCTION public.setup_new_user()
RETURNS TRIGGER AS $$
BEGIN
-- Use public.accounts
INSERT INTO public.accounts (id, name, slug, ...)
VALUES (...);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Key tables to prefix with public.:
profiles → public.profilesprofessional_profiles → public.professional_profilesdancer_profiles → public.dancer_profilesaccounts → public.accountsaccounts_memberships → public.accounts_membershipsSymptoms:
meteor_entity_mapping has duplicate entriesDebugging:
-- Check for duplicate mappings
SELECT meteor_id, entity_type, COUNT(*)
FROM meteor_entity_mapping
GROUP BY meteor_id, entity_type
HAVING COUNT(*) > 1;
-- Find entities without meteor_id
SELECT id, name, created_at
FROM organizations
WHERE meteor_id IS NULL
AND created_at > NOW() - INTERVAL '1 day';
Solution:
MeteorDuplicatePreventionService.findExistingEntity()_id is correctly passedSymptoms:
Debugging:
-- Find stale cache entries
SELECT mem.meteor_id, mem.entity_type, mem.entity_id
FROM meteor_entity_mapping mem
LEFT JOIN organizations o ON o.id = mem.entity_id
WHERE mem.entity_type = 'organization'
AND o.id IS NULL;
Solution:
-- Clean up stale entries
DELETE FROM meteor_entity_mapping mem
WHERE NOT EXISTS (
SELECT 1 FROM organizations WHERE id = mem.entity_id
)
AND entity_type = 'organization';
Symptoms:
Debugging:
# Check sync order
pnpm meteor:staging logs <run-id>
# Verify dependency exists
pnpm sql:local "SELECT * FROM organizations WHERE meteor_id = '<org_meteor_id>'"
Solution:
Symptoms:
Debugging:
-- Check source_hash in mapping
SELECT meteor_id, source_hash, last_synced_at
FROM meteor_entity_mapping
WHERE meteor_id = '<meteor_id>';
Solution:
MeteorChangeDetectorService.hasChanges()_id, meta)Symptoms:
Debugging:
-- Check for stuck runs
SELECT id, sync_type, status, started_at,
NOW() - started_at as duration
FROM meteor_sync_runs
WHERE status = 'running'
ORDER BY started_at DESC;
Solution:
-- Mark stuck run as failed
UPDATE meteor_sync_runs
SET status = 'failed',
completed_at = NOW(),
duration_seconds = EXTRACT(EPOCH FROM NOW() - started_at)::integer
WHERE status = 'running'
AND started_at < NOW() - INTERVAL '30 minutes';
Endpoint: /api/sync/meteor
All requests require Authorization: Bearer <METEOR_SYNC_API_KEY> header.
| Action | Description |
|---|---|
?action=status | Check MongoDB connection and environment |
?action=history&limit=10 | Get sync run history |
?action=stats | Get entity sync statistics |
?action=logs&runId=<id> | Get detailed logs for a sync run |
Trigger a sync:
{
"syncType": "full|incremental|entity",
"entityTypes": ["organization", "candidate"],
"dryRun": false,
"batchSize": 100,
"continueOnError": true
}
Response:
{
"success": true,
"syncRunId": "uuid",
"status": "running",
"startedAt": "2025-12-09T10:00:00Z"
}
Check overall sync health:
# From apps/web directory
cd apps/web && node scripts/meteor-diagnose.cjs
Checks:
Validate cache integrity (TypeScript version in skill folder):
# From apps/web directory with tsx available
cd apps/web && npx tsx ../../.claude/skills/meteor-sync-specialist/scripts/validate-entity-mapping.ts
Checks:
Compare MongoDB vs Supabase counts:
cd apps/web && npx tsx ../../.claude/skills/meteor-sync-specialist/scripts/compare-counts.ts
Output:
Entity Type MongoDB Supabase Diff Synced%
─────────────────────────────────────────────────────────
organization 428 425 -3 99.3%
candidate 20018 19854 -164 99.2%
media 75140 73278 -1862 97.5%
post 1492 1490 -2 99.9%
_id - Never rely on name/email matching_id, meta.*meteor_sync_changesmeteor_id columns// CORRECT - Validates entity exists
const existingId = await duplicatePrevention.findExistingEntity(
meteorId,
'organization'
);
if (existingId) {
// Entity exists and is valid
const hasChanges = changeDetector.hasChanges(newData, cachedData);
if (hasChanges) {
await updateEntity(existingId, newData);
}
} else {
// Create new entity
await createEntity(newData);
}
async function syncDocument(doc: MeteorDocument) {
const meteorId = doc._id.toString();
// Check if exists (validates entity still exists)
const existingId = await duplicatePrevention.findExistingEntity(
meteorId,
entityType
);
if (existingId) {
// Get cached data for comparison
const cachedData = await duplicatePrevention.getCachedSourceData(meteorId);
if (changeDetector.hasChanges(doc, cachedData)) {
await updateEntity(existingId, mapper.mapToSupabase(doc));
await duplicatePrevention.updateEntityMapping(meteorId, doc);
return { operation: 'updated' };
}
return { operation: 'skipped' };
} else {
const newId = await createEntity(mapper.mapToSupabase(doc));
await duplicatePrevention.registerEntity(meteorId, entityType, newId, doc);
return { operation: 'created' };
}
}
async function syncBatch(documents: MeteorDocument[]) {
const results = {
created: 0,
updated: 0,
skipped: 0,
failed: 0,
errors: [] as Array<{ meteorId: string; error: string }>
};
for (const doc of documents) {
try {
const result = await syncDocument(doc);
results[result.operation]++;
} catch (error) {
results.failed++;
results.errors.push({
meteorId: doc._id.toString(),
error: error.message
});
// Mark in entity mapping for retry
await duplicatePrevention.markEntityError(
doc._id.toString(),
entityType,
error.message
);
// Continue processing other documents
console.error(`Failed to sync ${doc._id}:`, error);
}
}
return results;
}
apps/web/app/admin/sync/_lib/server/meteor/meteor-sync.service.tsapps/web/app/admin/sync/_lib/server/meteor/meteor-duplicate-prevention.service.tsapps/web/app/admin/sync/_lib/server/meteor/meteor-change-detector.service.tsapps/web/app/admin/sync/_lib/server/meteor/media-transfer.service.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/index.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/organization.mapper.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/candidate.mapper.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/experience.mapper.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/dancer-media.mapper.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/organization-media.mapper.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/post.mapper.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/like.mapper.tsapps/web/app/admin/sync/_lib/server/meteor/mappers/follow.mapper.tsscripts/meteor-sync/run-sync.tsscripts/meteor-sync/staging-sync.tsscripts/sync-remaining-media.ts.claude/skills/meteor-sync-specialist/scripts/migrate-users.ts.claude/skills/meteor-sync-specialist/scripts/sync-orphan-media.ts.claude/skills/meteor-sync-specialist/scripts/sync-post-media.ts.claude/skills/meteor-sync-specialist/scripts/validate-entity-mapping.ts.claude/skills/meteor-sync-specialist/scripts/compare-counts.ts.claude/skills/meteor-sync-specialist/scripts/diagnose-sync-status.tsapps/web/app/api/sync/meteor/route.tsapps/web/app/admin/sync/meteor/page.tsxapps/web/app/admin/sync/meteor/_components/meteor-sync-trigger-panel.tsxapps/web/app/admin/sync/meteor/_components/meteor-sync-history.tsxapps/web/app/admin/sync/meteor/_components/meteor-sync-stats.tsxapps/web/app/admin/sync/meteor/_actions/meteor-sync.actions.tsapps/web/app/admin/sync/meteor/_actions/meteor-user-sync.actions.tsapps/web/supabase/migrations/20251207131700_create_meteor_sync_infrastructure.sqlapps/web/supabase/migrations/20251207145437_add_meteor_id_to_media_and_comments.sqlapps/web/supabase/migrations/20251207160100_add_meteor_id_to_community_tables.sqlapps/web/supabase/migrations/20251208183601_add_meteor_id_to_dancer_media.sqlscripts/meteor-sync/README.mdscripts/migration/README.mddocs/wip/active/WIP_achieving_100_percent_meteor_parity_2025_12_09.mddocs/wip/active/WIP_syncing_remaining_mongodb_media_2025_12_09.md.claude/skills/mongodb-production-query/SKILL.md.claude/skills/airtable-sync-specialist/SKILL.mdHandles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...
Comprehensive guides and best practices for Neon Serverless Postgres, covering setup, connection methods, authentication, and platform APIs.
Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules.
A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust.
Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.
Complete Better Auth server and client setup with database adapters, session management, plugins, and security configuration.
Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".
Complete Cloudflare platform integration with decision trees for compute, storage, AI, networking, security, and infrastructure-as-code.
Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.
Send, read, and manage Gmail messages, drafts, labels, and account settings.
Comprehensive website auditing across 230+ rules in 21 categories including SEO, performance, security, and accessibility.
Shared authentication, CLI syntax, and output formatting patterns for gws Google Workspace commands.
Discover your celebrity doppelgänger with StarByFace!
GeoSpy: Pricing, Features, FAQs, and Alternatives for AI Teams
Detect AI-generated voices to protect against audio fraud.
Ensure Your Content's Originality with AI Plagiarism Checker
Upscale images by 400% without quality loss
Protect your social media from copyright disputes
Accurately Detect AI-Generated Content with TheChecker.AI
Chrome extension that detects and flags AI-generated content