Skip to content

Commit d7a8876

Browse files
Tweak orbit/supabase setup
1 parent 9d273b3 commit d7a8876

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
@@ -43,7 +43,7 @@ Swach is a modern color palette manager built as a menubar/system tray Electron
4343
- **Build Tool**: Vite (via @embroider/vite for fast dev and optimized production)
4444
- **Desktop**: Electron with Electron Forge (menubar app using `menubar` package)
4545
- **Data Layer**: Orbit.js (client-side ORM with sync strategies)
46-
- **Storage**: IndexedDB (local), AWS Cognito + API Gateway (cloud sync)
46+
- **Storage**: IndexedDB (local), Supabase (auth + remote database)
4747
- **Color Picker**: Custom Rust binary for cross-platform pixel sampling
4848

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

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

9797
**Sync Strategies** (`app/data-strategies/`):
98-
Coordinate data flow between sources. Key strategies:
99-
- `store-backup-sync` - Persist all store changes to IndexedDB backup
100-
- `store-beforequery-remote-query` - Query remote before local queries (when authenticated)
101-
- `store-beforeupdate-remote-update` - Push local updates to remote (when authenticated)
102-
- `remote-store-sync` - Pull remote changes to store
98+
Coordinate data flow between sources. Orbit handles ALL data synchronization:
99+
- `store-backup-sync` - Persist all store changes to IndexedDB backup (blocking)
100+
- `store-beforequery-remote-query` - Query remote before local queries when authenticated (non-blocking)
101+
- `store-beforeupdate-remote-update` - Push local updates to remote when authenticated (non-blocking, optimistic UI)
102+
- `remote-store-sync` - Pull remote changes to store (blocking)
103103
- Error handling strategies for remote failures
104104

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

140146
### Authentication & Cloud Sync
141147

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

147154
**Session Service** (`app/services/session.ts`):
148-
- Wraps ember-simple-auth session
149-
- Provides `isAuthenticated` state
155+
- Manages authentication state
156+
- Provides `isAuthenticated` and `userId` properties
157+
158+
**Supabase Service** (`app/services/supabase.ts`):
159+
- Provides Supabase client instance
160+
- Used ONLY for auth and as remote API
161+
- No realtime subscriptions - all sync is handled by Orbit
150162

151-
**Remote Sync**:
163+
**Remote Sync** (`app/data-sources/remote.ts`):
152164
- Only activates when user is authenticated
153-
- JSON:API communication with AWS API Gateway
154-
- Coordinates palettes and colors bidirectionally
155-
- Handles conflict resolution (remote preferred for color history palette)
165+
- Implements Orbit source interface for Supabase backend
166+
- Transforms between Orbit records and Supabase rows
167+
- All synchronization coordinated by Orbit strategies, not Supabase realtime
156168

157169
### Component Structure
158170

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)