Skip to content

Commit cb9ca31

Browse files
authored
feat: powersync hs256 auth (#142)
* feat: powersync hs256 auth * fix: yarn lock update * fix: it WORKSSS!!!!!! * fix: label * fix: s/token/secret/g * docs: .env.example and openssl generate * fix: central token gen * fix: drop migration cruft
1 parent c68b224 commit cb9ca31

17 files changed

Lines changed: 252 additions & 47 deletions

.env.example

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ EXPO_PUBLIC_SUPABASE_BUCKET=your_supabase_bucket
55

66
# PowerSync Configuration
77
EXPO_PUBLIC_POWERSYNC_URL=your_powersync_url
8-
EXPO_PUBLIC_POWERSYNC_TOKEN=your_powersync_token
8+
# HS256 secret from PowerSync dashboard (base64url encoded)
9+
# Generate with: openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
10+
EXPO_PUBLIC_POWERSYNC_SECRET=your_powersync_hs256_secret
911

1012
# Sync Configuration
1113
EXPO_PUBLIC_SYNC_ENABLED=true
12-
EXPO_PUBLIC_SYNC_TYPE=unidirectional
14+
EXPO_PUBLIC_SYNC_TYPE=unidirectional

docs/DATA_SYNC_README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ This guide will help you set up data synchronization for Calendar Notifications
3131
3. Connect PowerSync to your Supabase database:
3232
- Follow the connection instructions in the PowerSync dashboard
3333
- Make sure to configure the necessary permissions and access controls
34-
4. Generate a developer token from the PowerSync dashboard
34+
4. Configure HS256 authentication:
35+
- Go to your instance settings → Client Auth tab
36+
- Under **"JWT Audience (optional)"**, click the "+" button and add: `powersync`
37+
- Under **"HS256 authentication tokens (ADVANCED)"**, click the "+" button
38+
- For **KID**, enter: `powersync`
39+
- For **HS256 secret**, generate one: `openssl rand -base64 32 | tr '+/' '-_' | tr -d '='`
40+
- Copy the generated secret (you'll need it for the app)
41+
- Click **"Save and deploy"**
3542

3643
### 3. Calendar Notifications Plus Configuration
3744

@@ -41,7 +48,9 @@ This guide will help you set up data synchronization for Calendar Notifications
4148
- Supabase URL (from step 1.3)
4249
- Supabase Anon Key (from step 1.3)
4350
- PowerSync Instance URL (from step 2.2)
44-
- PowerSync Token (from step 2.4)
51+
- PowerSync Token (paste the same HS256 secret from step 2.4)
52+
53+
> **Note:** The app automatically generates short-lived JWT tokens (5 min expiry) using this secret. The JWTs include `sub` (device ID), `aud` (powersync), `iat`, and `exp` claims. Unlike development tokens, the HS256 secret doesn't expire - you only need to configure it once.
4554
4655
## Verification
4756

@@ -55,6 +64,10 @@ This guide will help you set up data synchronization for Calendar Notifications
5564
- Check the PowerSync status in the app
5665
- Ensure your Supabase and PowerSync instances are running and connected
5766
- Review the app logs for any error messages
67+
- **JWT errors:** Make sure the HS256 secret in the app matches exactly what's configured in PowerSync dashboard
68+
- **"Invalid token" errors:** Verify the HS256 secret is properly configured under Client Auth → HS256 authentication tokens
69+
- **"Missing aud claim" errors:** Add `powersync` to JWT Audience in the PowerSync dashboard (Client Auth → JWT Audience)
70+
- **"Unexpected aud claim" errors:** The JWT Audience in PowerSync dashboard must include `powersync`
5871

5972
## Additional Resources
6073

env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ declare module '@env' {
44
export const EXPO_PUBLIC_SUPABASE_ANON_KEY: string | undefined;
55
export const EXPO_PUBLIC_SUPABASE_BUCKET: string | undefined;
66
export const EXPO_PUBLIC_POWERSYNC_URL: string | undefined;
7-
export const EXPO_PUBLIC_POWERSYNC_TOKEN: string | undefined;
7+
export const EXPO_PUBLIC_POWERSYNC_SECRET: string | undefined;
88
export const EXPO_PUBLIC_SYNC_ENABLED: string | undefined;
99
export const EXPO_PUBLIC_SYNC_TYPE: string | undefined;
1010
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@testing-library/react-native": "^13.3.3",
4242
"await-to-js": "^3.0.0",
4343
"commander": "^13.1.0",
44+
"crypto-js": "^4.2.0",
4445
"dom-helpers": "^5.2.1",
4546
"dotenv": "^16.3.1",
4647
"execa": "^9.5.2",
@@ -80,6 +81,7 @@
8081
"@swc-node/register": "^1.6.7",
8182
"@testing-library/react-hooks": "^8.0.1",
8283
"@tsconfig/react-native": "^3.0.2",
84+
"@types/crypto-js": "^4.2.2",
8385
"@types/jest": "^29.5.5",
8486
"@types/node": "^22.14.0",
8587
"@types/react": "~19.1.0",

src/lib/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
EXPO_PUBLIC_SUPABASE_ANON_KEY,
55
EXPO_PUBLIC_SUPABASE_BUCKET,
66
EXPO_PUBLIC_POWERSYNC_URL,
7-
EXPO_PUBLIC_POWERSYNC_TOKEN,
7+
EXPO_PUBLIC_POWERSYNC_SECRET,
88
EXPO_PUBLIC_SYNC_ENABLED,
99
EXPO_PUBLIC_SYNC_TYPE,
1010
} from '@env';
@@ -17,7 +17,7 @@ if (__DEV__) {
1717
SUPABASE_URL: EXPO_PUBLIC_SUPABASE_URL ? 'SET' : 'EMPTY',
1818
SUPABASE_ANON_KEY: EXPO_PUBLIC_SUPABASE_ANON_KEY ? 'SET' : 'EMPTY',
1919
POWERSYNC_URL: EXPO_PUBLIC_POWERSYNC_URL ? 'SET' : 'EMPTY',
20-
POWERSYNC_TOKEN: EXPO_PUBLIC_POWERSYNC_TOKEN ? 'SET' : 'EMPTY',
20+
POWERSYNC_SECRET: EXPO_PUBLIC_POWERSYNC_SECRET ? 'SET' : 'EMPTY',
2121
});
2222
}
2323

@@ -29,7 +29,7 @@ export const ConfigObj = {
2929
},
3030
powersync:{
3131
url: EXPO_PUBLIC_POWERSYNC_URL || '',
32-
token: EXPO_PUBLIC_POWERSYNC_TOKEN || ''
32+
secret: EXPO_PUBLIC_POWERSYNC_SECRET || ''
3333
},
3434
sync: {
3535
enabled: EXPO_PUBLIC_SYNC_ENABLED !== 'false',

src/lib/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const getEnv = () => {
2121
SUPABASE_ANON_KEY: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
2222
SUPABASE_BUCKET: process.env.EXPO_PUBLIC_SUPABASE_BUCKET,
2323
POWERSYNC_URL: process.env.EXPO_PUBLIC_POWERSYNC_URL,
24-
POWERSYNC_TOKEN: process.env.EXPO_PUBLIC_POWERSYNC_TOKEN,
24+
POWERSYNC_SECRET: process.env.EXPO_PUBLIC_POWERSYNC_SECRET,
2525
SYNC_ENABLED: process.env.EXPO_PUBLIC_SYNC_ENABLED,
2626
SYNC_TYPE: process.env.EXPO_PUBLIC_SYNC_TYPE,
2727
};

src/lib/features/SetupSync.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const isSettingsConfigured = (settings: Settings): boolean => Boolean(
4141
settings.supabaseUrl &&
4242
settings.supabaseAnonKey &&
4343
settings.powersyncUrl &&
44-
settings.powersyncToken
44+
settings.powersyncSecret
4545
);
4646

4747
export const SetupSync = () => {

src/lib/features/__tests__/SetupSync.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const createSettings = (overrides: Partial<Settings> = {}): Settings => ({
2424
supabaseUrl: '',
2525
supabaseAnonKey: '',
2626
powersyncUrl: '',
27-
powersyncToken: '',
27+
powersyncSecret: '',
2828
...overrides,
2929
});
3030

@@ -34,7 +34,7 @@ const createCompleteSettings = (overrides: Partial<Settings> = {}): Settings =>
3434
supabaseUrl: 'https://example.supabase.co',
3535
supabaseAnonKey: 'anon-key-123',
3636
powersyncUrl: 'https://example.powersync.com',
37-
powersyncToken: 'token123',
37+
powersyncSecret: 'token123',
3838
...overrides,
3939
});
4040

@@ -56,8 +56,8 @@ describe('SetupSync', () => {
5656
expect(isSettingsConfigured(createCompleteSettings({ powersyncUrl: '' }))).toBe(false);
5757
});
5858

59-
it('returns false when powersyncToken is missing', () => {
60-
expect(isSettingsConfigured(createCompleteSettings({ powersyncToken: '' }))).toBe(false);
59+
it('returns false when powersyncSecret is missing', () => {
60+
expect(isSettingsConfigured(createCompleteSettings({ powersyncSecret: '' }))).toBe(false);
6161
});
6262

6363
it('returns true when all credentials are provided', () => {

src/lib/features/__tests__/SetupSync.ui.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const testState = {
2323
supabaseUrl: '',
2424
supabaseAnonKey: '',
2525
powersyncUrl: '',
26-
powersyncToken: '',
26+
powersyncSecret: '',
2727
},
2828
powerSyncStatus: {
2929
connected: null as boolean | null,
@@ -78,7 +78,7 @@ const configureSettings = (configured: boolean) => {
7878
supabaseUrl: 'https://example.supabase.co',
7979
supabaseAnonKey: 'anon-key-123',
8080
powersyncUrl: 'https://example.powersync.com',
81-
powersyncToken: 'token123',
81+
powersyncSecret: 'token123',
8282
};
8383
} else {
8484
testState.settings = {
@@ -87,7 +87,7 @@ const configureSettings = (configured: boolean) => {
8787
supabaseUrl: '',
8888
supabaseAnonKey: '',
8989
powersyncUrl: '',
90-
powersyncToken: '',
90+
powersyncSecret: '',
9191
};
9292
}
9393
};
@@ -174,7 +174,7 @@ describe('SetupSync UI States', () => {
174174
supabaseUrl: 'https://example.supabase.co',
175175
supabaseAnonKey: 'anon-key-123',
176176
powersyncUrl: 'https://example.powersync.com',
177-
powersyncToken: 'token123',
177+
powersyncSecret: 'token123',
178178
};
179179
configurePowerSyncStatus(null);
180180
});

src/lib/features/__tests__/__mocks__/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const EXPO_PUBLIC_SUPABASE_URL = '';
66
export const EXPO_PUBLIC_SUPABASE_ANON_KEY = '';
77
export const EXPO_PUBLIC_SUPABASE_BUCKET = '';
88
export const EXPO_PUBLIC_POWERSYNC_URL = '';
9-
export const EXPO_PUBLIC_POWERSYNC_TOKEN = '';
9+
export const EXPO_PUBLIC_POWERSYNC_SECRET = '';
1010
export const EXPO_PUBLIC_SYNC_ENABLED = 'false';
1111
export const EXPO_PUBLIC_SYNC_TYPE = 'unidirectional';
1212

0 commit comments

Comments
 (0)