Skip to content

Commit b76f1ea

Browse files
Tweak orbit/supabase setup
1 parent 3b5197c commit b76f1ea

File tree

6 files changed

+562
-57
lines changed

6 files changed

+562
-57
lines changed

WARP.md

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Swach is a modern color palette manager built as a menubar/system tray Electron
3737
- **Build Tool**: Vite (via @embroider/vite for fast dev and optimized production)
3838
- **Desktop**: Electron with Electron Forge (menubar app using `menubar` package)
3939
- **Data Layer**: Orbit.js (client-side ORM with sync strategies)
40-
- **Storage**: IndexedDB (local), AWS Cognito + API Gateway (cloud sync)
40+
- **Storage**: IndexedDB (local), Supabase (auth + remote database)
4141
- **Color Picker**: hue-hunter package (cross-platform magnifying color picker with Rust-powered pixel sampling)
4242

4343
### Ember + Electron Integration
@@ -83,20 +83,26 @@ Swach uses Orbit.js for sophisticated offline-first data management with three s
8383
**Data Sources** (`app/data-sources/`):
8484
- `store` - In-memory cache (primary interface, ember-orbit Store)
8585
- `backup` - IndexedDB persistence (local backup)
86-
- `remote` - JSON:API remote sync (AWS API Gateway, authenticated users only)
86+
- `remote` - Supabase backend (authenticated users only)
8787

8888
**Models** (`app/data-models/`):
8989
- `palette` - Collection of colors with metadata (name, isColorHistory, isFavorite, isLocked, colorOrder array)
9090
- `color` - Individual color with RGBA values, computed hex/hsl/rgba getters
9191

9292
**Sync Strategies** (`app/data-strategies/`):
93-
Coordinate data flow between sources. Key strategies:
94-
- `store-backup-sync` - Persist all store changes to IndexedDB backup
95-
- `store-beforequery-remote-query` - Query remote before local queries (when authenticated)
96-
- `store-beforeupdate-remote-update` - Push local updates to remote (when authenticated)
97-
- `remote-store-sync` - Pull remote changes to store
93+
Coordinate data flow between sources. Orbit handles ALL data synchronization:
94+
- `store-backup-sync` - Persist all store changes to IndexedDB backup (blocking)
95+
- `store-beforequery-remote-query` - Query remote before local queries when authenticated (non-blocking)
96+
- `store-beforeupdate-remote-update` - Push local updates to remote when authenticated (non-blocking, optimistic UI)
97+
- `remote-store-sync` - Pull remote changes to store (blocking)
9898
- Error handling strategies for remote failures
9999

100+
**Sync Behavior**:
101+
- Initial sync on app startup fetches all palettes/colors from Supabase
102+
- Local changes are immediately reflected (optimistic UI), then synced to remote in background
103+
- Pull-based sync before queries ensures fresh data when needed
104+
- No realtime subscriptions - sync is handled via Orbit's strategies only
105+
100106
**Data Service** (`app/services/data.ts`):
101107
- Manages coordinator activation and synchronization
102108
- Ensures single color history palette exists
@@ -133,20 +139,26 @@ Coordinate data flow between sources. Key strategies:
133139

134140
### Authentication & Cloud Sync
135141

136-
**AWS Cognito** (`ember-cognito` addon):
137-
- User pools for authentication
138-
- Identity pools for AWS credentials
139-
- Config in `config/environment.js` (poolId, clientId, identityPoolId)
142+
**Supabase**:
143+
- Authentication via email OTP (passwordless)
144+
- Remote database (PostgreSQL) for palettes and colors
145+
- Row-level security ensures users only access their own data
146+
- Config in `config/environment.js` (supabaseUrl, supabaseAnonKey)
140147

141148
**Session Service** (`app/services/session.ts`):
142-
- Wraps ember-simple-auth session
143-
- Provides `isAuthenticated` state
149+
- Manages authentication state
150+
- Provides `isAuthenticated` and `userId` properties
151+
152+
**Supabase Service** (`app/services/supabase.ts`):
153+
- Provides Supabase client instance
154+
- Used ONLY for auth and as remote API
155+
- No realtime subscriptions - all sync is handled by Orbit
144156

145-
**Remote Sync**:
157+
**Remote Sync** (`app/data-sources/remote.ts`):
146158
- Only activates when user is authenticated
147-
- JSON:API communication with AWS API Gateway
148-
- Coordinates palettes and colors bidirectionally
149-
- Handles conflict resolution (remote preferred for color history palette)
159+
- Implements Orbit source interface for Supabase backend
160+
- Transforms between Orbit records and Supabase rows
161+
- All synchronization coordinated by Orbit strategies, not Supabase realtime
150162

151163
### Component Structure
152164

app/data-models/color.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type PaletteModel from '../data-models/palette.ts';
88

99
export default class ColorModel extends Model {
1010
@attr('datetime') createdAt!: string;
11+
@attr('datetime') updatedAt!: string;
1112
@attr('string') name!: string;
1213
@attr('number') r!: number;
1314
@attr('number') g!: number;

app/data-models/palette.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type ColorModel from './color.ts';
44

55
export default class PaletteModel extends Model {
66
@attr('datetime') createdAt!: string;
7+
@attr('datetime') updatedAt!: string;
78
@attr('number') index!: number;
89
@attr('boolean') isColorHistory!: boolean;
910
@attr('boolean') isFavorite!: boolean;

app/data-sources/remote.ts

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
RecordSchema,
1313
RecordTransformResult,
1414
} from '@orbit/records';
15-
import type { RealtimeChannel, SupabaseClient } from '@supabase/supabase-js';
15+
import type { SupabaseClient } from '@supabase/supabase-js';
1616

1717
import type SessionService from '../services/session.ts';
1818
import type SupabaseService from '../services/supabase.ts';
@@ -55,7 +55,6 @@ interface SupabaseSourceInjections {
5555
export class SupabaseSource extends Source {
5656
private supabaseService: SupabaseService;
5757
private session: SessionService;
58-
private realtimeChannel: RealtimeChannel | null = null;
5958

6059
constructor(injections: SupabaseSourceInjections) {
6160
super(injections);
@@ -379,15 +378,10 @@ export class SupabaseSource extends Source {
379378
relatedRecord: RecordIdentity
380379
): Promise<void> {
381380
if (record.type === 'palette' && relationship === 'colors') {
382-
// Colors cannot exist without a palette, so delete the color entirely
383-
// The database will handle this via ON DELETE CASCADE when the palette is deleted,
384-
// but for explicit removal operations, we delete the color record
385-
const { error } = await this.supabase
386-
.from('colors')
387-
.delete()
388-
.eq('id', relatedRecord.id);
389-
390-
if (error) throw new Error(`Supabase delete error: ${error.message}`);
381+
// No-op: Color moves between palettes should use replaceRelatedRecord/replaceRelatedRecords
382+
// to update the palette_id FK. Deleting here breaks color-move operations that
383+
// call removeFromRelatedRecords then addToRelatedRecords.
384+
// Colors are only truly deleted via explicit removeRecord operations.
391385
}
392386
}
393387

@@ -401,6 +395,7 @@ export class SupabaseSource extends Source {
401395
type: 'palette',
402396
attributes: {
403397
createdAt: palette.created_at,
398+
updatedAt: palette.updated_at,
404399
name: palette.name,
405400
isColorHistory: palette.is_color_history,
406401
isFavorite: palette.is_favorite,
@@ -424,6 +419,7 @@ export class SupabaseSource extends Source {
424419
type: 'color',
425420
attributes: {
426421
createdAt: color.created_at,
422+
updatedAt: color.updated_at,
427423
name: color.name,
428424
r: color.r,
429425
g: color.g,
@@ -489,35 +485,6 @@ export class SupabaseSource extends Source {
489485
private getTableName(type: string): string {
490486
return type === 'palette' ? 'palettes' : 'colors';
491487
}
492-
493-
// Set up real-time subscriptions
494-
setupRealtimeSync(onUpdate: (type: string, payload: unknown) => void): void {
495-
if (this.realtimeChannel) {
496-
void this.supabase.removeChannel(this.realtimeChannel);
497-
}
498-
499-
this.realtimeChannel = this.supabase
500-
.channel('db-changes')
501-
.on(
502-
'postgres_changes',
503-
{ event: '*', schema: 'public', table: 'palettes' },
504-
(payload) => onUpdate('palette', payload)
505-
)
506-
.on(
507-
'postgres_changes',
508-
{ event: '*', schema: 'public', table: 'colors' },
509-
(payload) => onUpdate('color', payload)
510-
)
511-
.subscribe();
512-
}
513-
514-
// Tear down real-time subscriptions
515-
teardownRealtimeSync(): void {
516-
if (this.realtimeChannel) {
517-
void this.supabase.removeChannel(this.realtimeChannel);
518-
this.realtimeChannel = null;
519-
}
520-
}
521488
}
522489

523490
export default {

0 commit comments

Comments
 (0)