├── CLAUDE.md ├── Examples ├── supabase │ ├── seed.sql │ ├── .gitignore │ ├── functions │ │ └── hello-world │ │ │ └── index.ts │ └── migrations │ │ └── 20221223094509_init.sql ├── SlackClone │ ├── supabase │ │ ├── seed.sql │ │ └── .gitignore │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── SlackClone.entitlements │ ├── SlackCloneApp.swift │ ├── Info.plist │ ├── Logger.swift │ ├── Supabase.swift │ ├── Dependencies.swift │ ├── ChannelListView.swift │ ├── AuthView.swift │ └── AppView.swift ├── UserManagement │ ├── supabase │ │ ├── seed.sql │ │ └── .gitignore │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── supabase-swift-demo.png │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── UserManagement.entitlements │ ├── UserManagementApp.swift │ ├── SwiftUIHelpers.swift │ ├── Models.swift │ ├── Info.plist │ ├── AppView.swift │ ├── Supabase.swift │ ├── AvatarImage.swift │ └── AuthView.swift ├── Examples │ ├── .ci │ │ └── pre_build.sh │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Stringfy.swift │ ├── Examples.entitlements │ ├── Supabase.plist │ ├── SupabaseConfig.swift │ ├── UIApplicationExtensions.swift │ ├── Constants.swift │ ├── ErrorText.swift │ ├── RootView.swift │ ├── Debug.swift │ ├── ThirdPartyAuthClerk.swift │ ├── UIViewControllerWrapper.swift │ ├── TodoListRow.swift │ ├── Models.swift │ ├── Auth │ │ ├── AuthController.swift │ │ ├── GoogleSignInSDKFlow.swift │ │ └── SignInWithFacebook.swift │ ├── AddTodoListView.swift │ ├── Info.plist │ ├── Shared │ │ └── GitHubSourceLink.swift │ ├── ActionState.swift │ ├── Realtime │ │ └── RealtimeExamplesView.swift │ ├── Storage │ │ ├── BucketList.swift │ │ └── FileObjectDetailView.swift │ └── HomeView.swift ├── Examples.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Package.swift ├── .github ├── CODEOWNERS ├── copilot-instructions.md ├── workflows │ ├── release.yml │ └── conventional-commits.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml ├── .serena └── .gitignore ├── Tests ├── StorageTests │ ├── Fixtures │ │ └── file.txt │ ├── sadcat.jpg │ ├── __Snapshots__ │ │ └── StorageBucketAPITests │ │ │ └── StorageBucketAPITests-testCreateBucket.1.txt │ ├── BucketOptionsTests.swift │ ├── SupabaseStorageClient+Test.swift │ ├── FileOptionsTests.swift │ ├── StorageErrorTests.swift │ └── TransformOptionsTests.swift ├── IntegrationTests │ ├── supabase │ │ ├── .temp │ │ │ └── cli-latest │ │ ├── .branches │ │ │ └── _current_branch │ │ └── .gitignore │ ├── Fixtures │ │ └── Upload │ │ │ ├── file-2.txt │ │ │ └── sadcat.jpg │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── DotEnv.swift │ └── StorageClientIntegrationTests.swift ├── FunctionsTests │ ├── __Snapshots__ │ │ └── RequestTests │ │ │ ├── testInvokeWithCustomMethod.1.txt │ │ │ ├── testInvokeWithDefaultOptions.1.txt │ │ │ ├── testInvokeWithCustomHeader.1.txt │ │ │ ├── testInvokeWithBody.1.txt │ │ │ └── testInvokeWithCustomRegion.1.txt │ ├── FunctionsErrorTests.swift │ ├── FunctionInvokeOptionsTests.swift │ └── RequestTests.swift ├── PostgRESTTests │ ├── __Snapshots__ │ │ └── BuildURLRequestTests │ │ │ ├── testBuildRequest.rpc-call-with-get.txt │ │ │ ├── testBuildRequest.rpc-call-with-head.txt │ │ │ ├── testBuildRequest.test-in-filter.txt │ │ │ ├── testBuildRequest.query-if-nil-value.txt │ │ │ ├── testBuildRequest.containedBy-using-array.txt │ │ │ ├── testBuildRequest.containedBy-using-range.txt │ │ │ ├── testBuildRequest.call-rpc-with-filter.txt │ │ │ ├── testBuildRequest.call-rpc-without-parameter.txt │ │ │ ├── testBuildRequest.containedBy-using-json.txt │ │ │ ├── testBuildRequest.filter-starting-with-non-alphanumeric.txt │ │ │ ├── testBuildRequest.filter-using-Date.txt │ │ │ ├── testBuildRequest.call-rpc.txt │ │ │ ├── testBuildRequest.iLikeAllOf.txt │ │ │ ├── testBuildRequest.iLikeAnyOf.txt │ │ │ ├── testBuildRequest.likeAllOf.txt │ │ │ ├── testBuildRequest.likeAnyOf.txt │ │ │ ├── testBuildRequest.query-non-default-schema.txt │ │ │ ├── testBuildRequest.rpc-call-with-get-and-params.txt │ │ │ ├── testBuildRequest.test-contains-filter-with-array.txt │ │ │ ├── testBuildRequest.select-all-users-where-email-ends-with-supabase-co.txt │ │ │ ├── testBuildRequest.test-contains-filter-with-dictionary.txt │ │ │ ├── testBuildRequest.insert-new-user.txt │ │ │ ├── testBuildRequest.test-or-filter-with-referenced-table.txt │ │ │ ├── testBuildRequest.query-with-character.txt │ │ │ ├── testBuildRequest.query-with-timestampz.txt │ │ │ ├── testBuildRequest.select-after-an-insert.txt │ │ │ ├── testBuildRequest.bulk-insert-users.txt │ │ │ ├── testBuildRequest.test-upsert-ignoring-duplicates.txt │ │ │ ├── testBuildRequest.test-upsert-not-ignoring-duplicates.txt │ │ │ ├── testBuildRequest.bulk-upsert.txt │ │ │ ├── testBuildRequest.select-after-bulk-upsert.txt │ │ │ └── testBuildRequest.test-all-filters-and-count.txt │ ├── JSONTests.swift │ ├── PostgrestFilterValueTests.swift │ ├── PostgresQueryTests.swift │ └── PostgrestResponseTests.swift ├── AuthTests │ ├── __Snapshots__ │ │ └── RequestsTests │ │ │ ├── testSessionFromURL.1.txt │ │ │ ├── testReauthenticate.1.txt │ │ │ ├── testMFAUnenroll.1.txt │ │ │ ├── testSignOut.1.txt │ │ │ ├── testMFAChallenge.1.txt │ │ │ ├── testSignOutWithLocalScope.1.txt │ │ │ ├── testSignOutWithOthersScope.1.txt │ │ │ ├── testUnlinkIdentity.1.txt │ │ │ ├── testVerifyOTPUsingTokenHash.1.txt │ │ │ ├── testRefreshSession.1.txt │ │ │ ├── testDeleteUser.1.txt │ │ │ ├── testSetSessionWithAExpiredToken.1.txt │ │ │ ├── testMFAChallengePhone.1.txt │ │ │ ├── testGetLinkIdentityURL.1.txt │ │ │ ├── testSignInAnonymously.1.txt │ │ │ ├── testResendPhone.1.txt │ │ │ ├── testMFAVerify.1.txt │ │ │ ├── testMFAEnrollLegacy.1.txt │ │ │ ├── testMFAEnrollTotp.1.txt │ │ │ ├── testResetPasswordForEmail.1.txt │ │ │ ├── testMFAEnrollPhone.1.txt │ │ │ ├── testVerifyOTPUsingPhone.1.txt │ │ │ ├── testSignInWithSSOUsingDomain.1.txt │ │ │ ├── testResendEmail.1.txt │ │ │ ├── testSignInWithPhoneAndPassword.1.txt │ │ │ ├── testSignInWithEmailAndPassword.1.txt │ │ │ ├── testSignInWithSSOUsingProviderId.1.txt │ │ │ ├── testVerifyOTPUsingEmail.1.txt │ │ │ ├── testSignInWithOTPUsingPhone.1.txt │ │ │ ├── testSignUpWithPhoneAndPassword.1.txt │ │ │ ├── testSignInWithOTPUsingEmail.1.txt │ │ │ ├── testSignUpWithEmailAndPassword.1.txt │ │ │ ├── testSignInWithIdToken.1.txt │ │ │ ├── testUpdateUser.1.txt │ │ │ └── testSetSessionWithAFutureExpirationDate.1.txt │ ├── AuthResponseTests.swift │ ├── Mocks │ │ └── Mocks.swift │ ├── Resources │ │ ├── signup-response.json │ │ ├── anonymous-sign-in-response.json │ │ ├── local-storage.json │ │ ├── user.json │ │ └── session.json │ ├── MockHelpers.swift │ ├── AuthClientMultipleInstancesTests.swift │ ├── ExtractParamsTests.swift │ └── PKCETests.swift ├── HelpersTests │ ├── PostgrestErrorTests.swift │ ├── JWTTests.swift │ ├── WithTimeoutTests.swift │ ├── ObservationTokenTests.swift │ └── EventEmitterTests.swift └── RealtimeTests │ ├── RealtimePostgresFilterValueTests.swift │ ├── ExportsTests.swift │ ├── RealtimeErrorTests.swift │ └── RealtimePostgresFilterTests.swift ├── .gitattributes ├── .release-please-manifest.json ├── supabase ├── .gitignore ├── migrations │ └── 20240327182636_init_key_value_storage_schema.sql └── seed.sql ├── Sources ├── Auth │ ├── Exports.swift │ ├── Storage │ │ ├── AuthLocalStorage.swift │ │ └── KeychainLocalStorage.swift │ ├── AuthStateChangeListener.swift │ ├── Internal │ │ ├── JWTAlgorithm.swift │ │ ├── URLOpener.swift │ │ ├── EventEmitter.swift │ │ ├── FixedWidthInteger+Random.swift │ │ ├── Helpers.swift │ │ ├── Dependencies.swift │ │ ├── Constants.swift │ │ ├── PKCE.swift │ │ └── CodeVerifierStorage.swift │ └── Defaults.swift ├── Realtime │ ├── Exports.swift │ ├── RealtimeError.swift │ ├── PostgresActionData.swift │ ├── RealtimePostgresFilterValue.swift │ ├── RealtimePostgresFilter.swift │ └── PushV2.swift ├── Storage │ ├── Exports.swift │ ├── StorageError.swift │ ├── Codable.swift │ ├── StorageHTTPClient.swift │ ├── BucketOptions.swift │ ├── SupabaseStorage.swift │ └── TransformOptions.swift ├── Functions │ └── Exports.swift ├── PostgREST │ ├── Exports.swift │ └── Defaults.swift ├── TestHelpers │ ├── Exports.swift │ ├── AsyncSequence.swift │ ├── WithMainSerialExecutor+Windows.swift │ ├── InMemoryLocalStorage.swift │ ├── MockExtensions.swift │ └── HTTPClientMock.swift ├── Supabase │ ├── Exports.swift │ ├── Constants.swift │ └── Deprecated.swift └── Helpers │ ├── SharedModels │ ├── PostgrestError.swift │ └── HTTPError.swift │ ├── HTTP │ ├── HTTPResponse.swift │ ├── HTTPClient.swift │ ├── LoggerInterceptor.swift │ └── HTTPFields.swift │ ├── Task+withTimeout.swift │ ├── Base64URL.swift │ ├── Codable.swift │ ├── TaskLocalHelpers.swift │ ├── _Clock.swift │ ├── Logger │ └── OSLogSupabaseLogger.swift │ ├── JWT.swift │ └── Version.swift ├── scripts ├── run-on-linux.sh ├── load_env.sh └── check-for-breaking-api-changes.sh ├── .editorconfig ├── .spi.yml ├── .env.example ├── .vscode └── settings.json ├── Supabase.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── release-please-config.json └── LICENSE /CLAUDE.md: -------------------------------------------------------------------------------- 1 | @AGENTS.md 2 | -------------------------------------------------------------------------------- /Examples/supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grdsdev 2 | -------------------------------------------------------------------------------- /.serena/.gitignore: -------------------------------------------------------------------------------- 1 | /cache 2 | -------------------------------------------------------------------------------- /Examples/SlackClone/supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Examples/UserManagement/supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/StorageTests/Fixtures/file.txt: -------------------------------------------------------------------------------- 1 | hello world! 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /Tests/**/__Snapshots__/**/*.txt eol=lf 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.38.1" 3 | } -------------------------------------------------------------------------------- /Tests/IntegrationTests/supabase/.temp/cli-latest: -------------------------------------------------------------------------------- 1 | v2.22.12 -------------------------------------------------------------------------------- /Tests/IntegrationTests/supabase/.branches/_current_branch: -------------------------------------------------------------------------------- 1 | main -------------------------------------------------------------------------------- /Examples/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /Tests/IntegrationTests/Fixtures/Upload/file-2.txt: -------------------------------------------------------------------------------- 1 | supabase txt file 2 2 | -------------------------------------------------------------------------------- /Examples/Examples/.ci/pre_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp _Secrets.swift Secrets.swift -------------------------------------------------------------------------------- /Examples/SlackClone/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /Examples/UserManagement/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /Tests/StorageTests/sadcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/supabase-swift/HEAD/Tests/StorageTests/sadcat.jpg -------------------------------------------------------------------------------- /Tests/IntegrationTests/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Examples/Examples/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/SlackClone/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/UserManagement/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/IntegrationTests/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /Examples/UserManagement/supabase-swift-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/supabase-swift/HEAD/Examples/UserManagement/supabase-swift-demo.png -------------------------------------------------------------------------------- /Examples/Examples/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/IntegrationTests/Fixtures/Upload/sadcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/supabase-swift/HEAD/Tests/IntegrationTests/Fixtures/Upload/sadcat.jpg -------------------------------------------------------------------------------- /Examples/SlackClone/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/UserManagement/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Auth/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 30/05/25. 6 | // 7 | 8 | @_exported import Helpers 9 | -------------------------------------------------------------------------------- /Sources/Realtime/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 30/05/25. 6 | // 7 | 8 | @_exported import Helpers 9 | -------------------------------------------------------------------------------- /Sources/Storage/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 30/05/25. 6 | // 7 | 8 | @_exported import Helpers 9 | -------------------------------------------------------------------------------- /Sources/Functions/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 30/05/25. 6 | // 7 | 8 | @_exported import Helpers 9 | -------------------------------------------------------------------------------- /Sources/PostgREST/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 30/05/25. 6 | // 7 | 8 | @_exported import Helpers 9 | -------------------------------------------------------------------------------- /Sources/TestHelpers/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 30/05/25. 6 | // 7 | 8 | @_exported import Helpers 9 | -------------------------------------------------------------------------------- /scripts/run-on-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SWIFT_VERSION="latest" 4 | 5 | # Spin Swift Docker container 6 | docker run -it --rm -v $(pwd):/app -w /app "swift:$SWIFT_VERSION" bash -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | ident_style = space 7 | ident_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | -------------------------------------------------------------------------------- /Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Examples/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/SlackClone/SlackClone.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | documentation_targets: 6 | - Supabase 7 | - Auth 8 | - Storage 9 | - PostgREST 10 | - Realtime 11 | - Functions -------------------------------------------------------------------------------- /Examples/SlackClone/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/UserManagement/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/UserManagement/UserManagement.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomMethod.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request PATCH \ 3 | --header "apikey: supabase.anon.key" \ 4 | --header "x-client-info: functions-swift/x.y.z" \ 5 | "http://localhost:5432/functions/v1/hello-world" -------------------------------------------------------------------------------- /Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithDefaultOptions.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "apikey: supabase.anon.key" \ 4 | --header "x-client-info: functions-swift/x.y.z" \ 5 | "http://localhost:5432/functions/v1/hello-world" -------------------------------------------------------------------------------- /supabase/migrations/20240327182636_init_key_value_storage_schema.sql: -------------------------------------------------------------------------------- 1 | create table key_value_storage( 2 | "key" text primary key, 3 | "value" jsonb not null 4 | ); 5 | 6 | alter publication supabase_realtime 7 | add table key_value_storage; 8 | 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Get these from your API settings: https://supabase.com/dashboard/project/_/settings/api 2 | 3 | SUPABASE_URL=https://mysupabasereference.supabase.co 4 | SUPABASE_ANON_KEY=my.supabase.anon.key 5 | SUPABASE_SERVICE_ROLE_KEY=my.supabase.service.role.key -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "apikey", 4 | "HTTPURL", 5 | "pkce", 6 | "postgrest", 7 | "preconcurrency", 8 | "Supabase", 9 | "whitespaces", 10 | "xctest" 11 | ], 12 | "makefile.configureOnOpen": false 13 | } 14 | -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/rpc/sum" -------------------------------------------------------------------------------- /Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomHeader.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "apikey: supabase.anon.key" \ 4 | --header "x-client-info: functions-swift/x.y.z" \ 5 | --header "x-custom-key: custom value" \ 6 | "http://localhost:5432/functions/v1/hello-world" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-head.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --head \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 6 | "https://example.supabase.co/rpc/sum" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-in-filter.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/todos?id=in.(1,2,3)&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-if-nil-value.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?email=is.NULL&select=*" -------------------------------------------------------------------------------- /Supabase.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Apikey: dummy.api.key" \ 3 | --header "Authorization: bearer accesstoken" \ 4 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 5 | --header "X-Supabase-Api-Version: 2024-01-01" \ 6 | "http://localhost:54321/auth/v1/user" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.containedBy-using-array.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?id=cd.%7Ba,b,c%7D&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.containedBy-using-range.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?age=cd.%5B10,20%5D&select=*" -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "simple", 5 | "extra-files": [ 6 | "Sources/Helpers/Version.swift" 7 | ] 8 | } 9 | }, 10 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 11 | } -------------------------------------------------------------------------------- /Supabase.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testReauthenticate.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Apikey: dummy.api.key" \ 3 | --header "Authorization: Bearer accesstoken" \ 4 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 5 | --header "X-Supabase-Api-Version: 2024-01-01" \ 6 | "http://localhost:54321/auth/v1/reauthenticate" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.call-rpc-with-filter.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 6 | "https://example.supabase.co/rpc/test_fcn?id=eq.1" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.call-rpc-without-parameter.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 6 | "https://example.supabase.co/rpc/test_fcn" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.containedBy-using-json.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?select=*&userMetadata=cd.%7B%22age%22:18%7D" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.filter-starting-with-non-alphanumeric.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?select=*&to=eq.+16505555555" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.filter-using-Date.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?created_at=gt.1970-01-01T00:00:00.000Z&select=*" -------------------------------------------------------------------------------- /Examples/Examples/Stringfy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stringfy.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 21/03/24. 6 | // 7 | 8 | import CustomDump 9 | import Foundation 10 | 11 | func stringfy(_ value: Any) -> String { 12 | var output = "" 13 | customDump(value, to: &output) 14 | return output 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Supabase/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 30/05/25. 6 | // 7 | 8 | @_exported import Auth 9 | @_exported import Functions 10 | @_exported import Helpers 11 | @_exported import PostgREST 12 | @_exported import Realtime 13 | @_exported import Storage 14 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testMFAUnenroll.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request DELETE \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | "http://localhost:54321/auth/v1/factors/123" -------------------------------------------------------------------------------- /Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithBody.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Content-Type: application/json" \ 4 | --header "apikey: supabase.anon.key" \ 5 | --header "x-client-info: functions-swift/x.y.z" \ 6 | --data "{\"name\":\"Supabase\"}" \ 7 | "http://localhost:5432/functions/v1/hello-world" -------------------------------------------------------------------------------- /Tests/FunctionsTests/__Snapshots__/RequestTests/testInvokeWithCustomRegion.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "apikey: supabase.anon.key" \ 4 | --header "x-client-info: functions-swift/x.y.z" \ 5 | --header "x-region: ap-northeast-1" \ 6 | "http://localhost:5432/functions/v1/hello-world?forceFunctionRegion=ap-northeast-1" -------------------------------------------------------------------------------- /Tests/StorageTests/__Snapshots__/StorageBucketAPITests/StorageBucketAPITests-testCreateBucket.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: storage-swift/2.24.4" \ 5 | --data "{\"public\":true,\"id\":\"newbucket\",\"name\":\"newbucket\"}" \ 6 | "http://example.com/bucket" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | "http://localhost:54321/auth/v1/logout?scope=global" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.call-rpc.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 6 | --data "{\"KEY\":\"VALUE\"}" \ 7 | "https://example.supabase.co/rpc/test_fcn" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.iLikeAllOf.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?email=ilike(all).%7B%25@supabase.io,%25@supabase.com%7D&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.iLikeAnyOf.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?email=ilike(any).%7B%25@supabase.io,%25@supabase.com%7D&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.likeAllOf.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?email=like(all).%7B%25@supabase.io,%25@supabase.com%7D&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.likeAnyOf.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?email=like(any).%7B%25@supabase.io,%25@supabase.com%7D&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-non-default-schema.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Accept-Profile: storage" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 6 | "https://example.supabase.co/objects?select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.rpc-call-with-get-and-params.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/rpc/get_array_element?array=%7B37,420,64%7D&index=2" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-contains-filter-with-array.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?name=cs.%7Bis:online,faction:red%7D&select=*" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallenge.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | "http://localhost:54321/auth/v1/factors/123/challenge" -------------------------------------------------------------------------------- /Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/TestHelpers/AsyncSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSequence.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 04/04/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension AsyncSequence { 11 | package func collect() async rethrows -> [Element] { 12 | try await reduce(into: [Element]()) { $0.append($1) } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | "http://localhost:54321/auth/v1/logout?scope=local" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.select-all-users-where-email-ends-with-supabase-co.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?email=like.%25@supabase.co&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-contains-filter-with-dictionary.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?address=cs.%7B%22postcode%22:90210%7D&select=name" -------------------------------------------------------------------------------- /Examples/UserManagement/UserManagementApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserManagementApp.swift 3 | // UserManagement 4 | // 5 | // Created by Guilherme Souza on 17/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct UserManagementApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | AppView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | "http://localhost:54321/auth/v1/logout?scope=others" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.insert-new-user.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 6 | --data "{\"email\":\"johndoe@supabase.io\"}" \ 7 | "https://example.supabase.co/users" -------------------------------------------------------------------------------- /Examples/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import Foundation 5 | import PackageDescription 6 | 7 | var package = Package( 8 | name: "Examples", 9 | platforms: [], 10 | products: [], 11 | dependencies: [], 12 | targets: [] 13 | ) 14 | -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-or-filter-with-referenced-table.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?messages.or=(public.eq.true,recipient_id.eq.1)&select=*,messages(*)" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-character.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/users?id=eq.Cig%C3%A1nyka-%C3%A9r%20(0+400%20cskm)%20v%C3%ADzrajzi%20%C3%A1llom%C3%A1s&select=*" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-with-timestampz.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/tasks?order=received_at.asc.nullslast&received_at=gt.2023-03-23T15:50:30.511743+00:00&select=*" -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot Instructions for supabase-swift 2 | 3 | ## Instructions for Code Review 4 | 5 | - Check for breaking API changes 6 | - Check if documentation is up to date 7 | - Check if examples are up to date 8 | - Check if tests are comprehensive and cover all critical paths 9 | 10 | ## Repository Overview 11 | 12 | @../AGENTS.md 13 | 14 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testUnlinkIdentity.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request DELETE \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | "http://localhost:54321/auth/v1/user/identities/E621E1F8-C36C-495A-93FC-0C247A3E6E5F" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingTokenHash.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"token_hash\":\"abc-def\",\"type\":\"email\"}" \ 8 | "http://localhost:54321/auth/v1/verify" -------------------------------------------------------------------------------- /Examples/UserManagement/SwiftUIHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIHelpers.swift 3 | // UserManagement 4 | // 5 | // Created by Guilherme Souza on 17/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | func onMac(_ block: (Self) -> some View) -> some View { 12 | #if os(macOS) 13 | return block(self) 14 | #else 15 | return self 16 | #endif 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"refresh_token\":\"refresh-token\"}" \ 8 | "http://localhost:54321/auth/v1/token?grant_type=refresh_token" -------------------------------------------------------------------------------- /Examples/Examples/Examples.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | keychain-access-groups 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testDeleteUser.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request DELETE \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"should_soft_delete\":false}" \ 8 | "http://localhost:54321/auth/v1/admin/users/E621E1F8-C36C-495A-93FC-0C247A3E6E5F" -------------------------------------------------------------------------------- /Examples/Examples/Supabase.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SUPABASE_URL 6 | http://127.0.0.1:54321 7 | SUPABASE_ANON_KEY 8 | sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"refresh_token\":\"dummy-refresh-token\"}" \ 8 | "http://localhost:54321/auth/v1/token?grant_type=refresh_token" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.select-after-an-insert.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "Prefer: return=representation" \ 6 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 7 | --data "{\"email\":\"johndoe@supabase.io\"}" \ 8 | "https://example.supabase.co/users?select=id,email" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testMFAChallengePhone.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "Content-Type: application/json" \ 6 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 7 | --header "X-Supabase-Api-Version: 2024-01-01" \ 8 | --data "{\"channel\":\"whatsapp\"}" \ 9 | "http://localhost:54321/auth/v1/factors/123/challenge" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testGetLinkIdentityURL.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Apikey: dummy.api.key" \ 3 | --header "Authorization: Bearer accesstoken" \ 4 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 5 | --header "X-Supabase-Api-Version: 2024-01-01" \ 6 | "http://localhost:54321/auth/v1/user/identities/authorize?extra_key=extra_value&provider=github&redirect_to=https://supabase.com&scopes=user:email&skip_http_redirect=true" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.bulk-insert-users.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 6 | --data "[{\"email\":\"johndoe@supabase.io\"},{\"email\":\"johndoe2@supabase.io\",\"username\":\"johndoe2\"}]" \ 7 | "https://example.supabase.co/users?columns=email,username" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-upsert-ignoring-duplicates.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "Prefer: resolution=ignore-duplicates,return=representation" \ 6 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 7 | --data "{\"email\":\"johndoe@supabase.io\"}" \ 8 | "https://example.supabase.co/users" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-upsert-not-ignoring-duplicates.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "Prefer: resolution=merge-duplicates,return=representation" \ 6 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 7 | --data "{\"email\":\"johndoe@supabase.io\"}" \ 8 | "https://example.supabase.co/users" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInAnonymously.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"}}" \ 8 | "http://localhost:54321/auth/v1/signup" -------------------------------------------------------------------------------- /Sources/TestHelpers/WithMainSerialExecutor+Windows.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WithMainSerialExecutor+Windows.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 12/03/24. 6 | // 7 | 8 | import Foundation 9 | 10 | #if os(Windows) 11 | /// Calling this method on Windows has no effect. 12 | public func withMainSerialExecutor( 13 | @_implicitSelfCapture operation: () throws -> Void 14 | ) rethrows { 15 | try operation() 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"type\":\"phone_change\"}" \ 8 | "http://localhost:54321/auth/v1/resend" -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release/* 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | release-please: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: googleapis/release-please-action@v4 19 | id: release 20 | with: 21 | target-branch: ${{ github.ref_name }} 22 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testMFAVerify.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "Content-Type: application/json" \ 6 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 7 | --header "X-Supabase-Api-Version: 2024-01-01" \ 8 | --data "{\"challenge_id\":\"123\",\"code\":\"123456\",\"factor_id\":\"123\"}" \ 9 | "http://localhost:54321/auth/v1/factors/123/verify" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollLegacy.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "Content-Type: application/json" \ 6 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 7 | --header "X-Supabase-Api-Version: 2024-01-01" \ 8 | --data "{\"factor_type\":\"totp\",\"friendly_name\":\"test\",\"issuer\":\"supabase.com\"}" \ 9 | "http://localhost:54321/auth/v1/factors" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollTotp.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "Content-Type: application/json" \ 6 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 7 | --header "X-Supabase-Api-Version: 2024-01-01" \ 8 | --data "{\"factor_type\":\"totp\",\"friendly_name\":\"test\",\"issuer\":\"supabase.com\"}" \ 9 | "http://localhost:54321/auth/v1/factors" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"}}" \ 8 | "http://localhost:54321/auth/v1/recover?redirect_to=https://supabase.com" -------------------------------------------------------------------------------- /scripts/load_env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | env_file="${1:-.env}" 4 | 5 | if [ ! -f "$env_file" ]; then 6 | echo "Error: Environment file '$env_file' not found." >&2 7 | exit 1 8 | fi 9 | 10 | while IFS= read -r line; do 11 | line="$(echo "${line%%#*}" | xargs)" 12 | if [ -n "$line" ]; then 13 | export "$line" || { 14 | echo "Error exporting: $line" >&2 15 | exit 1 16 | } 17 | fi 18 | done <"$env_file" 19 | -------------------------------------------------------------------------------- /Examples/SlackClone/SlackCloneApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlackCloneApp.swift 3 | // SlackClone 4 | // 5 | // Created by Guilherme Souza on 27/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | @MainActor 12 | struct SlackCloneApp: App { 13 | let model = AppViewModel() 14 | 15 | var body: some Scene { 16 | WindowGroup { 17 | AppView(model: model) 18 | .onOpenURL { url in 19 | supabase.handle(url) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testMFAEnrollPhone.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "Content-Type: application/json" \ 6 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 7 | --header "X-Supabase-Api-Version: 2024-01-01" \ 8 | --data "{\"factor_type\":\"phone\",\"friendly_name\":\"test\",\"phone\":\"+1 202-918-2132\"}" \ 9 | "http://localhost:54321/auth/v1/factors" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"token\":\"123456\",\"type\":\"sms\"}" \ 8 | "http://localhost:54321/auth/v1/verify" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"domain\":\"supabase.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"redirect_to\":\"https:\/\/supabase.com\"}" \ 8 | "http://localhost:54321/auth/v1/sso" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"type\":\"email_change\"}" \ 8 | "http://localhost:54321/auth/v1/resend?redirect_to=https://supabase.com" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \ 8 | "http://localhost:54321/auth/v1/token?grant_type=password" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\"}" \ 8 | "http://localhost:54321/auth/v1/token?grant_type=password" -------------------------------------------------------------------------------- /Tests/HelpersTests/PostgrestErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostgrestErrorTests.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 07/05/24. 6 | // 7 | 8 | import Foundation 9 | import Helpers 10 | import XCTest 11 | 12 | final class PostgrestErrorTests: XCTestCase { 13 | 14 | func testLocalizedErrorConformance() { 15 | let error = PostgrestError(message: "test error message") 16 | XCTAssertEqual(error.errorDescription, "test error message") 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.bulk-upsert.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "Prefer: resolution=merge-duplicates,return=representation" \ 6 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 7 | --data "[{\"email\":\"johndoe@supabase.io\"},{\"email\":\"johndoe2@supabase.io\",\"username\":\"johndoe2\"}]" \ 8 | "https://example.supabase.co/users?columns=email,username" -------------------------------------------------------------------------------- /Examples/Examples/SupabaseConfig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum SupabaseConfig { 4 | static subscript(key: String) -> String? { 5 | guard let plistFileURL = Bundle.main.url(forResource: "Supabase", withExtension: "plist"), 6 | let plistData = try? Data(contentsOf: plistFileURL), 7 | let plist = try? PropertyListSerialization.propertyList(from: plistData, format: nil) 8 | as? [String: Any] 9 | else { return nil } 10 | 11 | return plist[key] as? String 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/UserManagement/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // UserManagement 4 | // 5 | // Created by Guilherme Souza on 17/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Profile: Codable { 11 | let username: String? 12 | let fullName: String? 13 | let website: String? 14 | let avatarURL: String? 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case username 18 | case fullName = "full_name" 19 | case website 20 | case avatarURL = "avatar_url" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"provider_id\":\"E621E1F8-C36C-495A-93FC-0C247A3E6E5F\",\"redirect_to\":\"https:\/\/supabase.com\"}" \ 8 | "http://localhost:54321/auth/v1/sso" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.select-after-bulk-upsert.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Accept: application/json" \ 4 | --header "Content-Type: application/json" \ 5 | --header "Prefer: resolution=merge-duplicates,return=representation" \ 6 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 7 | --data "[{\"email\":\"johndoe@supabase.io\"},{\"email\":\"johndoe2@supabase.io\"}]" \ 8 | "https://example.supabase.co/users?columns=email&on_conflict=username&select=*" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"token\":\"123456\",\"type\":\"magiclink\"}" \ 8 | "http://localhost:54321/auth/v1/verify?redirect_to=https://supabase.com" -------------------------------------------------------------------------------- /Examples/UserManagement/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | io.supabase.user-management 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"channel\":\"sms\",\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"phone\":\"+1 202-918-2132\"}" \ 8 | "http://localhost:54321/auth/v1/otp" -------------------------------------------------------------------------------- /Tests/IntegrationTests/DotEnv.swift: -------------------------------------------------------------------------------- 1 | enum DotEnv { 2 | static let SUPABASE_URL = "http://127.0.0.1:54321" 3 | static let SUPABASE_ANON_KEY = 4 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" 5 | static let SUPABASE_SERVICE_ROLE_KEY = 6 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU" 7 | } 8 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"channel\":\"sms\",\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \ 8 | "http://localhost:54321/auth/v1/signup" -------------------------------------------------------------------------------- /Examples/Examples/UIApplicationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplicationExtensions.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 05/03/24. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | extension UIApplication { 12 | var firstKeyWindow: UIWindow? { 13 | UIApplication.shared 14 | .connectedScenes 15 | .compactMap { $0 as? UIWindowScene } 16 | .filter { $0.activationState == .foregroundActive } 17 | .first?.keyWindow 18 | } 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"}}" \ 8 | "http://localhost:54321/auth/v1/otp?redirect_to=https://supabase.com" -------------------------------------------------------------------------------- /Tests/IntegrationTests/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "denoland.vscode-deno" 4 | }, 5 | "deno.enablePaths": [ 6 | "supabase/functions" 7 | ], 8 | "deno.lint": true, 9 | "deno.unstable": [ 10 | "bare-node-builtins", 11 | "byonm", 12 | "sloppy-imports", 13 | "unsafe-proto", 14 | "webgpu", 15 | "broadcast-channel", 16 | "worker-options", 17 | "cron", 18 | "kv", 19 | "ffi", 20 | "fs", 21 | "http", 22 | "net" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /Examples/Examples/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 15/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Constants { 11 | static let redirectToURL = URL(string: "com.supabase.swift-examples://")! 12 | } 13 | 14 | extension URL { 15 | init?(scheme: String) { 16 | var components = URLComponents() 17 | components.scheme = scheme 18 | 19 | guard let url = components.url else { 20 | return nil 21 | } 22 | 23 | self = url 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\"}" \ 8 | "http://localhost:54321/auth/v1/signup?redirect_to=https://supabase.com" -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request POST \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Content-Type: application/json" \ 5 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 6 | --header "X-Supabase-Api-Version: 2024-01-01" \ 7 | --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"link_identity\":false,\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ 8 | "http://localhost:54321/auth/v1/token?grant_type=id_token" -------------------------------------------------------------------------------- /Sources/Storage/StorageError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct StorageError: Error, Decodable, Sendable { 4 | public var statusCode: String? 5 | public var message: String 6 | public var error: String? 7 | 8 | public init(statusCode: String? = nil, message: String, error: String? = nil) { 9 | self.statusCode = statusCode 10 | self.message = message 11 | self.error = error 12 | } 13 | } 14 | 15 | extension StorageError: LocalizedError { 16 | public var errorDescription: String? { 17 | message 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Supabase/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 06/03/25. 6 | // 7 | 8 | import Foundation 9 | 10 | let defaultHeaders: [String: String] = { 11 | var headers = [ 12 | "X-Client-Info": "supabase-swift/\(version)" 13 | ] 14 | 15 | if let platform { 16 | headers["X-Supabase-Client-Platform"] = platform 17 | } 18 | 19 | if let platformVersion { 20 | headers["X-Supabase-Client-Platform-Version"] = platformVersion 21 | } 22 | 23 | return headers 24 | }() 25 | -------------------------------------------------------------------------------- /Sources/Realtime/RealtimeError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimeError.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 30/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RealtimeError: LocalizedError { 11 | var errorDescription: String? 12 | 13 | init(_ errorDescription: String) { 14 | self.errorDescription = errorDescription 15 | } 16 | } 17 | 18 | extension RealtimeError { 19 | /// The maximum retry attempts reached. 20 | static var maxRetryAttemptsReached: Self { 21 | Self("Maximum retry attempts reached.") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --request PUT \ 3 | --header "Apikey: dummy.api.key" \ 4 | --header "Authorization: Bearer accesstoken" \ 5 | --header "Content-Type: application/json" \ 6 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 7 | --header "X-Supabase-Api-Version: 2024-01-01" \ 8 | --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"email_change_token\":\"123456\",\"nonce\":\"abcdef\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \ 9 | "http://localhost:54321/auth/v1/user" -------------------------------------------------------------------------------- /Examples/Examples/ErrorText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorText.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 23/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ErrorText: View { 11 | let error: Error 12 | 13 | init(_ error: Error) { 14 | self.error = error 15 | } 16 | 17 | var body: some View { 18 | Text(error.localizedDescription) 19 | .foregroundColor(.red) 20 | .font(.footnote) 21 | } 22 | } 23 | 24 | struct ErrorText_Previews: PreviewProvider { 25 | static var previews: some View { 26 | ErrorText(NSError()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Auth/Storage/AuthLocalStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol AuthLocalStorage: Sendable { 4 | func store(key: String, value: Data) throws 5 | func retrieve(key: String) throws -> Data? 6 | func remove(key: String) throws 7 | } 8 | 9 | extension AuthClient.Configuration { 10 | #if !os(Linux) && !os(Windows) && !os(Android) 11 | public static let defaultLocalStorage: any AuthLocalStorage = KeychainLocalStorage() 12 | #elseif os(Windows) 13 | public static let defaultLocalStorage: any AuthLocalStorage = WinCredLocalStorage() 14 | #endif 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Realtime/PostgresActionData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostgresActionData.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 26/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PostgresActionData: Codable { 11 | var type: String 12 | var record: [String: AnyJSON]? 13 | var oldRecord: [String: AnyJSON]? 14 | var columns: [Column] 15 | var commitTimestamp: Date 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case type 19 | case record 20 | case oldRecord = "old_record" 21 | case columns 22 | case commitTimestamp = "commit_timestamp" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/Examples/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootView.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 22/12/22. 6 | // 7 | 8 | import Auth 9 | import SwiftUI 10 | 11 | struct RootView: View { 12 | @Environment(AuthController.self) var auth 13 | 14 | var body: some View { 15 | if auth.session == nil { 16 | NavigationStack { 17 | AuthExamplesView() 18 | } 19 | } else { 20 | HomeView() 21 | } 22 | } 23 | } 24 | 25 | struct ContentView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | RootView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Examples/Examples/Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debug.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 15/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | func debug( 11 | _ message: @autoclosure () -> String, 12 | function: String = #function, 13 | file: String = #file, 14 | line: UInt = #line 15 | ) { 16 | assert( 17 | { 18 | let fileHandle = FileHandle.standardError 19 | 20 | let logLine = "[\(function) \(file.split(separator: "/").last!):\(line)] \(message())\n" 21 | fileHandle.write(Data(logLine.utf8)) 22 | 23 | return true 24 | }() 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /Tests/FunctionsTests/FunctionsErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionsErrorTests.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 20/01/25. 6 | // 7 | 8 | import Supabase 9 | import XCTest 10 | 11 | final class FunctionsErrorTests: XCTestCase { 12 | 13 | func testLocalizedDescription() { 14 | XCTAssertEqual( 15 | FunctionsError.relayError.localizedDescription, "Relay Error invoking the Edge Function") 16 | XCTAssertEqual( 17 | FunctionsError.httpError(code: 412, data: Data()).localizedDescription, 18 | "Edge Function returned a non-2xx status code: 412") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/SlackClone/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLIconFile 11 | 12 | CFBundleURLName 13 | com.supabase.slack-clone 14 | CFBundleURLSchemes 15 | 16 | com.supabase.slack-clone 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/RealtimeTests/RealtimePostgresFilterValueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimePostgresFilterValueTests.swift 3 | // Supabase 4 | // 5 | // Created by Lucas Abijmil on 19/02/2025. 6 | // 7 | 8 | import XCTest 9 | @testable import Realtime 10 | 11 | final class RealtimePostgresFilterValueTests: XCTestCase { 12 | func testUUID() { 13 | XCTAssertEqual( 14 | UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!.rawValue, 15 | "E621E1F8-C36C-495A-93FC-0C247A3E6E5F") 16 | } 17 | 18 | func testDate() { 19 | XCTAssertEqual( 20 | Date(timeIntervalSince1970: 1_737_465_985).rawValue, 21 | "2025-01-21T13:26:25.000Z" 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/Examples/ThirdPartyAuthClerk.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThirdPartyAuthClerk.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 26/03/25. 6 | // 7 | 8 | import Clerk 9 | import Foundation 10 | import Supabase 11 | 12 | extension SupabaseClient { 13 | static let thirdPartyAuthWithClerk = SupabaseClient( 14 | supabaseURL: URL(string: SupabaseConfig["SUPABASE_URL"]!)!, 15 | supabaseKey: SupabaseConfig["SUPABASE_ANON_KEY"]!, 16 | options: SupabaseClientOptions( 17 | auth: SupabaseClientOptions.AuthOptions( 18 | accessToken: { 19 | try await Clerk.shared.session?.getToken()?.jwt 20 | } 21 | ) 22 | ) 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /Tests/AuthTests/AuthResponseTests.swift: -------------------------------------------------------------------------------- 1 | import Auth 2 | import SnapshotTesting 3 | import XCTest 4 | 5 | final class AuthResponseTests: XCTestCase { 6 | func testSession() throws { 7 | let response = try AuthClient.Configuration.jsonDecoder.decode( 8 | AuthResponse.self, 9 | from: json(named: "session") 10 | ) 11 | XCTAssertNotNil(response.session) 12 | XCTAssertEqual(response.user, response.session?.user) 13 | } 14 | 15 | func testUser() throws { 16 | let response = try AuthClient.Configuration.jsonDecoder.decode( 17 | AuthResponse.self, 18 | from: json(named: "user") 19 | ) 20 | XCTAssertNil(response.session) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Apikey: dummy.api.key" \ 3 | --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjo0ODUyMTYzNTkzLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.UiEhoahP9GNrBKw_OHBWyqYudtoIlZGkrjs7Qa8hU7I" \ 4 | --header "X-Client-Info: gotrue-swift/x.y.z" \ 5 | --header "X-Supabase-Api-Version: 2024-01-01" \ 6 | "http://localhost:54321/auth/v1/user" -------------------------------------------------------------------------------- /Tests/PostgRESTTests/JSONTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONTests.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 01/07/24. 6 | // 7 | 8 | @testable import PostgREST 9 | import XCTest 10 | 11 | final class JSONTests: XCTestCase { 12 | func testDecodeJSON() throws { 13 | let json = """ 14 | { 15 | "created_at": "2024-06-15T18:12:04+00:00" 16 | } 17 | """.data(using: .utf8)! 18 | 19 | struct Value: Decodable { 20 | var createdAt: Date 21 | 22 | enum CodingKeys: String, CodingKey { 23 | case createdAt = "created_at" 24 | } 25 | } 26 | _ = try PostgrestClient.Configuration.jsonDecoder.decode(Value.self, from: json) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/HelpersTests/JWTTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Helpers 3 | 4 | final class JWTTests: XCTestCase { 5 | func testDecodeJWT() throws { 6 | let token = 7 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.CGr5zNE5Yltlbn_3Ms2cjSLs_AW9RKM3lxh7cTQrg0w" 8 | let jwt = JWT.decodePayload(token) 9 | let exp = try XCTUnwrap(jwt?["exp"] as? TimeInterval) 10 | XCTAssertEqual(exp, 1_648_640_021) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "swift" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "npm" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /Examples/UserManagement/AppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppView.swift 3 | // UserManagement 4 | // 5 | // Created by Guilherme Souza on 17/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppView: View { 11 | @State var isAuthenticated = false 12 | 13 | var body: some View { 14 | Group { 15 | if isAuthenticated { 16 | ProfileView() 17 | } else { 18 | AuthView() 19 | } 20 | } 21 | .task { 22 | for await state in supabase.auth.authStateChanges { 23 | if [.initialSession, .signedIn, .signedOut].contains(state.event) { 24 | isAuthenticated = state.session != nil 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | #Preview { 32 | AppView() 33 | } 34 | -------------------------------------------------------------------------------- /Sources/TestHelpers/InMemoryLocalStorage.swift: -------------------------------------------------------------------------------- 1 | import Auth 2 | import ConcurrencyExtras 3 | import Foundation 4 | 5 | package final class InMemoryLocalStorage: AuthLocalStorage, @unchecked Sendable { 6 | let _storage = LockIsolated([String: Data]()) 7 | 8 | package var storage: [String: Data] { 9 | _storage.value 10 | } 11 | 12 | package init() {} 13 | 14 | package func store(key: String, value: Data) throws { 15 | _storage.withValue { 16 | $0[key] = value 17 | } 18 | } 19 | 20 | package func retrieve(key: String) throws -> Data? { 21 | _storage.value[key] 22 | } 23 | 24 | package func remove(key: String) throws { 25 | _storage.withValue { 26 | $0[key] = nil 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Supabase/Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deprecated.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 15/05/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension SupabaseClient { 11 | /// Database client for Supabase. 12 | @available( 13 | *, 14 | deprecated, 15 | message: "Direct access to database is deprecated, please use one of the available methods such as, SupabaseClient.from(_:), SupabaseClient.rpc(_:params:), or SupabaseClient.schema(_:)." 16 | ) 17 | public var database: PostgrestClient { 18 | rest 19 | } 20 | 21 | /// Realtime client for Supabase 22 | @available(*, deprecated, message: "Use realtimeV2") 23 | public var realtime: RealtimeClient { 24 | _realtime.value 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Examples/Examples/UIViewControllerWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewControllerWrapper.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 10/04/24. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import SwiftUI 10 | 11 | struct UIViewControllerWrapper: UIViewControllerRepresentable { 12 | typealias UIViewControllerType = T 13 | 14 | let viewController: T 15 | 16 | init(_ viewController: T) { 17 | self.viewController = viewController 18 | } 19 | 20 | func makeUIViewController(context _: Context) -> T { 21 | viewController 22 | } 23 | 24 | func updateUIViewController(_: T, context _: Context) { 25 | // Update the view controller if needed 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Tests/StorageTests/BucketOptionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import Storage 4 | 5 | final class BucketOptionsTests: XCTestCase { 6 | func testDefaultInitialization() { 7 | let options = BucketOptions() 8 | 9 | XCTAssertFalse(options.public) 10 | XCTAssertNil(options.fileSizeLimit) 11 | XCTAssertNil(options.allowedMimeTypes) 12 | } 13 | 14 | func testCustomInitialization() { 15 | let options = BucketOptions( 16 | public: true, 17 | fileSizeLimit: "5242880", 18 | allowedMimeTypes: ["image/jpeg", "image/png"] 19 | ) 20 | 21 | XCTAssertTrue(options.public) 22 | XCTAssertEqual(options.fileSizeLimit, "5242880") 23 | XCTAssertEqual(options.allowedMimeTypes, ["image/jpeg", "image/png"]) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Helpers/SharedModels/PostgrestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostgrestError.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 27/01/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct PostgrestError: Error, Codable, Sendable { 11 | public let detail: String? 12 | public let hint: String? 13 | public let code: String? 14 | public let message: String 15 | 16 | public init( 17 | detail: String? = nil, 18 | hint: String? = nil, 19 | code: String? = nil, 20 | message: String 21 | ) { 22 | self.hint = hint 23 | self.detail = detail 24 | self.code = code 25 | self.message = message 26 | } 27 | } 28 | 29 | extension PostgrestError: LocalizedError { 30 | public var errorDescription: String? { 31 | message 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Auth/AuthStateChangeListener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthStateChangeListener.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 17/02/24. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | 11 | 12 | /// A listener that can be removed by calling ``AuthStateChangeListenerRegistration/remove()``. 13 | /// 14 | /// - Note: Listener is automatically removed on deinit. 15 | public protocol AuthStateChangeListenerRegistration: Sendable { 16 | /// Removes the listener. After the initial call, subsequent calls have no effect. 17 | func remove() 18 | } 19 | 20 | extension ObservationToken: AuthStateChangeListenerRegistration {} 21 | 22 | public typealias AuthStateChangeListener = @Sendable ( 23 | _ event: AuthChangeEvent, 24 | _ session: Session? 25 | ) -> Void 26 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/JWTAlgorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTVerifier.swift 3 | // Supabase 4 | // 5 | // Created by Claude on 06/10/25. 6 | // 7 | 8 | import Foundation 9 | 10 | enum JWTAlgorithm: String { 11 | case rs256 = "RS256" 12 | 13 | func verify( 14 | jwt: DecodedJWT, 15 | jwk: JWK 16 | ) -> Bool { 17 | let message = "\(jwt.raw.header).\(jwt.raw.payload)".data(using: .utf8)! 18 | switch self { 19 | case .rs256: 20 | #if canImport(Security) 21 | return SecKeyVerifySignature( 22 | jwk.rsaPublishKey!, 23 | .rsaSignatureMessagePKCS1v15SHA256, 24 | message as CFData, 25 | jwt.signature as CFData, 26 | nil 27 | ) 28 | #else 29 | return false 30 | #endif 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Storage/Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 18/10/23. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | 11 | extension JSONEncoder { 12 | @available(*, deprecated, message: "Access to storage encoder is going to be removed.") 13 | public static let defaultStorageEncoder: JSONEncoder = { 14 | let encoder = JSONEncoder() 15 | encoder.keyEncodingStrategy = .convertToSnakeCase 16 | return encoder 17 | }() 18 | 19 | static let unconfiguredEncoder: JSONEncoder = .init() 20 | } 21 | 22 | extension JSONDecoder { 23 | @available(*, deprecated, message: "Access to storage decoder is going to be removed.") 24 | public static let defaultStorageDecoder: JSONDecoder = { 25 | JSONDecoder.supabase() 26 | }() 27 | } 28 | -------------------------------------------------------------------------------- /Tests/StorageTests/SupabaseStorageClient+Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SupabaseStorageClient+Test.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 04/11/23. 6 | // 7 | 8 | import Foundation 9 | import Storage 10 | 11 | extension SupabaseStorageClient { 12 | static func test( 13 | supabaseURL: String, 14 | apiKey: String, 15 | session: StorageHTTPSession = .init() 16 | ) -> SupabaseStorageClient { 17 | SupabaseStorageClient( 18 | configuration: StorageClientConfiguration( 19 | url: URL(string: supabaseURL)!, 20 | headers: [ 21 | "Authorization": "Bearer \(apiKey)", 22 | "Apikey": apiKey, 23 | "X-Client-Info": "storage-swift/x.y.z", 24 | ], 25 | session: session, 26 | logger: nil 27 | ) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PostgREST/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 14/12/23. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | 11 | let version = Helpers.version 12 | 13 | extension PostgrestClient.Configuration { 14 | /// The default `JSONDecoder` instance for ``PostgrestClient`` responses. 15 | public static let jsonDecoder: JSONDecoder = { 16 | JSONDecoder.supabase() 17 | }() 18 | 19 | /// The default `JSONEncoder` instance for ``PostgrestClient`` requests. 20 | public static let jsonEncoder: JSONEncoder = { 21 | JSONEncoder.supabase() 22 | }() 23 | 24 | /// The default headers for ``PostgrestClient`` requests. 25 | public static let defaultHeaders: [String: String] = [ 26 | "X-Client-Info": "postgrest-swift/\(version)" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/URLOpener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLOpener.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 17/05/24. 6 | // 7 | 8 | import Foundation 9 | 10 | #if canImport(WatchKit) 11 | import WatchKit 12 | #endif 13 | 14 | #if canImport(UIKit) 15 | import UIKit 16 | #endif 17 | 18 | #if canImport(AppKit) 19 | import AppKit 20 | #endif 21 | 22 | struct URLOpener { 23 | var open: @MainActor @Sendable (_ url: URL) -> Void 24 | } 25 | 26 | extension URLOpener { 27 | static var live: Self { 28 | URLOpener { url in 29 | #if os(macOS) 30 | NSWorkspace.shared.open(url) 31 | #elseif os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) 32 | UIApplication.shared.open(url) 33 | #elseif os(watchOS) 34 | WKExtension.shared().openSystemURL(url) 35 | #endif 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/amannn/action-semantic-pull-request 2 | name: 'PR Title is Conventional' 3 | 4 | on: 5 | pull_request: 6 | types: 7 | - opened 8 | - edited 9 | - synchronize 10 | 11 | permissions: 12 | pull-requests: write 13 | contents: read 14 | 15 | jobs: 16 | main: 17 | name: Validate PR title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v6 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | subjectPattern: ^(?![A-Z]).+$ 25 | subjectPatternError: | 26 | The subject "{subject}" found in the pull request title "{title}" 27 | didn't match the configured pattern. Please ensure that the subject 28 | doesn't start with an uppercase character. 29 | -------------------------------------------------------------------------------- /Examples/Examples/TodoListRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoListRow.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 23/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TodoListRow: View { 11 | let todo: Todo 12 | let completeTapped: () -> Void 13 | 14 | var body: some View { 15 | HStack { 16 | Text(todo.description) 17 | Spacer() 18 | Button { 19 | completeTapped() 20 | } label: { 21 | Image(systemName: todo.isComplete ? "checkmark.circle.fill" : "circle") 22 | } 23 | .buttonStyle(.plain) 24 | } 25 | } 26 | } 27 | 28 | struct TodoListRow_Previews: PreviewProvider { 29 | static var previews: some View { 30 | TodoListRow( 31 | todo: .init( 32 | id: UUID(), 33 | description: "", 34 | isComplete: false, 35 | createdAt: .now 36 | ) 37 | ) {} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-all-filters-and-count.txt: -------------------------------------------------------------------------------- 1 | curl \ 2 | --header "Accept: application/json" \ 3 | --header "Content-Type: application/json" \ 4 | --header "X-Client-Info: postgrest-swift/x.y.z" \ 5 | "https://example.supabase.co/todos?column=eq.Some%20value&column=neq.Some%20value&column=gt.Some%20value&column=gte.Some%20value&column=lt.Some%20value&column=lte.Some%20value&column=like.Some%20value&column=ilike.Some%20value&column=match.Some%20value&column=imatch.Some%20value&column=is.Some%20value&column=isdistinct.Some%20value&column=in.Some%20value&column=cs.Some%20value&column=cd.Some%20value&column=sl.Some%20value&column=sr.Some%20value&column=nxl.Some%20value&column=nxr.Some%20value&column=adj.Some%20value&column=ov.Some%20value&column=fts.Some%20value&column=plfts.Some%20value&column=phfts.Some%20value&column=wfts.Some%20value&select=*" -------------------------------------------------------------------------------- /Sources/Auth/Storage/KeychainLocalStorage.swift: -------------------------------------------------------------------------------- 1 | #if !os(Windows) && !os(Linux) && !os(Android) 2 | import Foundation 3 | 4 | /// ``AuthLocalStorage`` implementation using Keychain. This is the default local storage used by the library. 5 | public struct KeychainLocalStorage: AuthLocalStorage { 6 | private let keychain: Keychain 7 | 8 | public init(service: String? = "supabase.gotrue.swift", accessGroup: String? = nil) { 9 | keychain = Keychain(service: service, accessGroup: accessGroup) 10 | } 11 | 12 | public func store(key: String, value: Data) throws { 13 | try keychain.set(value, forKey: key) 14 | } 15 | 16 | public func retrieve(key: String) throws -> Data? { 17 | try keychain.data(forKey: key) 18 | } 19 | 20 | public func remove(key: String) throws { 21 | try keychain.deleteItem(forKey: key) 22 | } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /Tests/StorageTests/FileOptionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import Storage 4 | 5 | final class FileOptionsTests: XCTestCase { 6 | func testDefaultInitialization() { 7 | let options = FileOptions() 8 | 9 | XCTAssertEqual(options.cacheControl, "3600") 10 | XCTAssertNil(options.contentType) 11 | XCTAssertFalse(options.upsert) 12 | XCTAssertNil(options.metadata) 13 | } 14 | 15 | func testCustomInitialization() { 16 | let metadata: [String: AnyJSON] = ["key": .string("value")] 17 | let options = FileOptions( 18 | cacheControl: "7200", 19 | contentType: "image/jpeg", 20 | upsert: true, 21 | metadata: metadata 22 | ) 23 | 24 | XCTAssertEqual(options.cacheControl, "7200") 25 | XCTAssertEqual(options.contentType, "image/jpeg") 26 | XCTAssertTrue(options.upsert) 27 | XCTAssertEqual(options.metadata?["key"], .string("value")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Examples/SlackClone/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // SlackClone 4 | // 5 | // Created by Guilherme Souza on 23/01/24. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import Supabase 11 | 12 | extension Logger { 13 | static let main = Self(subsystem: "com.supabase.slack-clone", category: "app") 14 | static let supabase = Self(subsystem: "com.supabase.slack-clone", category: "supabase") 15 | } 16 | 17 | struct SupaLogger: SupabaseLogger { 18 | func log(message: SupabaseLogMessage) { 19 | Task { @MainActor in 20 | let logger = Logger.supabase 21 | 22 | switch message.level { 23 | case .debug: logger.debug("\(message, privacy: .public)") 24 | case .error: logger.error("\(message, privacy: .public)") 25 | case .verbose: logger.info("\(message, privacy: .public)") 26 | case .warning: logger.notice("\(message, privacy: .public)") 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Storage/StorageHTTPClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | public struct StorageHTTPSession: Sendable { 8 | public var fetch: @Sendable (_ request: URLRequest) async throws -> (Data, URLResponse) 9 | public var upload: 10 | @Sendable (_ request: URLRequest, _ data: Data) async throws -> (Data, URLResponse) 11 | 12 | public init( 13 | fetch: @escaping @Sendable (_ request: URLRequest) async throws -> (Data, URLResponse), 14 | upload: @escaping @Sendable (_ request: URLRequest, _ data: Data) async throws -> ( 15 | Data, URLResponse 16 | ) 17 | ) { 18 | self.fetch = fetch 19 | self.upload = upload 20 | } 21 | 22 | public init(session: URLSession = .shared) { 23 | self.init( 24 | fetch: { try await session.data(for: $0) }, 25 | upload: { try await session.upload(for: $0, from: $1) } 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/HelpersTests/WithTimeoutTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WithTimeoutTests.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 19/04/24. 6 | // 7 | 8 | import Foundation 9 | import Helpers 10 | import XCTest 11 | 12 | final class WithTimeoutTests: XCTestCase { 13 | // func testWithTimeout() async { 14 | // do { 15 | // try await withTimeout(interval: 0.25) { 16 | // try await Task.sleep(nanoseconds: NSEC_PER_SEC) 17 | // } 18 | // XCTFail("Task should timeout.") 19 | // } catch { 20 | // XCTAssertTrue(error is TimeoutError) 21 | // } 22 | // 23 | // do { 24 | // let answer = try await withTimeout(interval: 1.25) { 25 | // try await Task.sleep(nanoseconds: NSEC_PER_SEC) 26 | // return 42 27 | // } 28 | // 29 | // XCTAssertEqual(answer, 42) 30 | // } catch { 31 | // XCTFail("Should not throw error: \(error)") 32 | // } 33 | // } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/RealtimeTests/ExportsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportsTests.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 29/07/25. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import Realtime 11 | 12 | final class ExportsTests: XCTestCase { 13 | func testHelperImportIsAccessible() { 14 | // Test that the Helpers module is properly exported 15 | // This is a simple validation that the @_exported import works 16 | 17 | // Test that we can access JSONObject from Helpers via Realtime 18 | let jsonObject: JSONObject = [:] 19 | XCTAssertNotNil(jsonObject) 20 | 21 | // Test that we can access AnyJSON from Helpers via Realtime 22 | let anyJSON: AnyJSON = .string("test") 23 | XCTAssertEqual(anyJSON, .string("test")) 24 | 25 | // Test that we can access ObservationToken from Helpers via Realtime 26 | let token = ObservationToken { 27 | // Empty cleanup 28 | } 29 | XCTAssertNotNil(token) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Helpers/HTTP/HTTPResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPResponse.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 30/04/24. 6 | // 7 | 8 | import Foundation 9 | import HTTPTypes 10 | 11 | #if canImport(FoundationNetworking) 12 | import FoundationNetworking 13 | #endif 14 | 15 | package struct HTTPResponse: Sendable { 16 | package let data: Data 17 | package let headers: HTTPFields 18 | package let statusCode: Int 19 | 20 | package let underlyingResponse: HTTPURLResponse 21 | 22 | package init(data: Data, response: HTTPURLResponse) { 23 | self.data = data 24 | headers = HTTPFields(response.allHeaderFields as? [String: String] ?? [:]) 25 | statusCode = response.statusCode 26 | underlyingResponse = response 27 | } 28 | } 29 | 30 | extension HTTPResponse { 31 | package func decoded(as _: T.Type = T.self, decoder: JSONDecoder = JSONDecoder()) throws -> T { 32 | try decoder.decode(T.self, from: data) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/AuthTests/Mocks/Mocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mocks.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 27/10/23. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | import TestHelpers 11 | import XCTestDynamicOverlay 12 | 13 | @testable import Auth 14 | 15 | let clientURL = URL(string: "http://localhost:54321/auth/v1")! 16 | 17 | extension Session { 18 | static let validSession = Session( 19 | accessToken: "accesstoken", 20 | tokenType: "bearer", 21 | expiresIn: 120, 22 | expiresAt: Date().addingTimeInterval(120).timeIntervalSince1970, 23 | refreshToken: "refreshtoken", 24 | user: User(fromMockNamed: "user") 25 | ) 26 | 27 | static let expiredSession = Session( 28 | accessToken: "accesstoken", 29 | tokenType: "bearer", 30 | expiresIn: 30, 31 | expiresAt: Date().addingTimeInterval(30).timeIntervalSince1970, 32 | refreshToken: "refreshtoken", 33 | user: User(fromMockNamed: "user") 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /Examples/Examples/Models.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Todo: Identifiable, Hashable, Decodable { 4 | let id: UUID 5 | var description: String 6 | var isComplete: Bool 7 | let createdAt: Date 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case id 11 | case description 12 | case isComplete = "is_complete" 13 | case createdAt = "created_at" 14 | } 15 | } 16 | 17 | struct CreateTodoRequest: Encodable { 18 | var description: String 19 | var isComplete: Bool 20 | var ownerID: UUID 21 | 22 | enum CodingKeys: String, CodingKey { 23 | case description 24 | case isComplete = "is_complete" 25 | case ownerID = "owner_id" 26 | } 27 | } 28 | 29 | struct UpdateTodoRequest: Encodable { 30 | var description: String? 31 | var isComplete: Bool? 32 | var ownerID: UUID 33 | 34 | enum CodingKeys: String, CodingKey { 35 | case description 36 | case isComplete = "is_complete" 37 | case ownerID = "owner_id" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Helpers/SharedModels/HTTPError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPError.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 07/05/24. 6 | // 7 | 8 | import Foundation 9 | 10 | #if canImport(FoundationNetworking) 11 | import FoundationNetworking 12 | #endif 13 | 14 | /// A generic error from a HTTP request. 15 | /// 16 | /// Contains both the `Data` and `HTTPURLResponse` which you can use to extract more information about it. 17 | public struct HTTPError: Error, Sendable { 18 | public let data: Data 19 | public let response: HTTPURLResponse 20 | 21 | public init(data: Data, response: HTTPURLResponse) { 22 | self.data = data 23 | self.response = response 24 | } 25 | } 26 | 27 | extension HTTPError: LocalizedError { 28 | public var errorDescription: String? { 29 | var message = "Status Code: \(response.statusCode)" 30 | if let body = String(data: data, encoding: .utf8) { 31 | message += " Body: \(body)" 32 | } 33 | return message 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Helpers/Task+withTimeout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task+withTimeout.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 19/04/24. 6 | // 7 | 8 | import Foundation 9 | 10 | @discardableResult 11 | package func withTimeout( 12 | interval: TimeInterval, 13 | @_inheritActorContext operation: @escaping @Sendable () async -> R 14 | ) async throws -> R { 15 | try await withThrowingTaskGroup(of: R.self) { group in 16 | defer { 17 | group.cancelAll() 18 | } 19 | 20 | let deadline = Date(timeIntervalSinceNow: interval) 21 | 22 | group.addTask { 23 | await operation() 24 | } 25 | 26 | group.addTask { 27 | let interval = deadline.timeIntervalSinceNow 28 | if interval > 0 { 29 | try await _clock.sleep(for: interval) 30 | } 31 | try Task.checkCancellation() 32 | throw TimeoutError() 33 | } 34 | 35 | return try await group.next()! 36 | } 37 | } 38 | 39 | package struct TimeoutError: Error, Hashable {} 40 | -------------------------------------------------------------------------------- /Tests/HelpersTests/ObservationTokenTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservationTokenTests.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 17/02/24. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | import Helpers 11 | import XCTest 12 | 13 | final class ObservationTokenTests: XCTestCase { 14 | func testRemove() { 15 | let handle = ObservationToken() 16 | 17 | let onRemoveCallCount = LockIsolated(0) 18 | handle.onCancel = { 19 | onRemoveCallCount.withValue { 20 | $0 += 1 21 | } 22 | } 23 | 24 | handle.cancel() 25 | handle.cancel() 26 | 27 | XCTAssertEqual(onRemoveCallCount.value, 1) 28 | } 29 | 30 | func testDeinit() { 31 | var handle: ObservationToken? = ObservationToken() 32 | 33 | let onRemoveCallCount = LockIsolated(0) 34 | handle?.onCancel = { 35 | onRemoveCallCount.withValue { 36 | $0 += 1 37 | } 38 | } 39 | 40 | handle = nil 41 | 42 | XCTAssertEqual(onRemoveCallCount.value, 1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/EventEmitter.swift: -------------------------------------------------------------------------------- 1 | import ConcurrencyExtras 2 | import Foundation 3 | 4 | struct AuthStateChangeEventEmitter { 5 | var emitter = EventEmitter<(AuthChangeEvent, Session?)?>(initialEvent: nil, emitsLastEventWhenAttaching: false) 6 | var logger: (any SupabaseLogger)? 7 | 8 | func attach(_ listener: @escaping AuthStateChangeListener) -> ObservationToken { 9 | emitter.attach { event in 10 | guard let event else { return } 11 | listener(event.0, event.1) 12 | 13 | logger?.verbose("Auth state changed: \(event)") 14 | } 15 | } 16 | 17 | func emit(_ event: AuthChangeEvent, session: Session?, token: ObservationToken? = nil) { 18 | NotificationCenter.default.post( 19 | name: AuthClient.didChangeAuthStateNotification, 20 | object: nil, 21 | userInfo: [ 22 | AuthClient.authChangeEventInfoKey: event, 23 | AuthClient.authChangeSessionInfoKey: session as Any, 24 | ] 25 | ) 26 | 27 | emitter.emit((event, session), to: token) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/FixedWidthInteger+Random.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Borrowed from the Vapor project, 4 | // https://github.com/vapor/vapor/blob/main/Sources/Vapor/Utilities/Array%2BRandom.swift#L14 5 | extension FixedWidthInteger { 6 | static func random() -> Self { 7 | random(in: .min ... .max) 8 | } 9 | 10 | static func random(using generator: inout some RandomNumberGenerator) -> Self { 11 | random(in: .min ... .max, using: &generator) 12 | } 13 | } 14 | 15 | extension Array where Element: FixedWidthInteger { 16 | static func random(count: Int) -> [Element] { 17 | var array: [Element] = .init(repeating: 0, count: count) 18 | (0 ..< count).forEach { array[$0] = Element.random() } 19 | return array 20 | } 21 | 22 | static func random(count: Int, using generator: inout some RandomNumberGenerator) -> [Element] { 23 | var array: [Element] = .init(repeating: 0, count: count) 24 | (0 ..< count).forEach { array[$0] = Element.random(using: &generator) } 25 | return array 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Examples/supabase/functions/hello-world/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts" 6 | 7 | console.log("Hello from Functions!") 8 | 9 | serve(async (req) => { 10 | const { name } = await req.json() 11 | const data = { 12 | message: `Hello ${name}!`, 13 | } 14 | 15 | return new Response( 16 | JSON.stringify(data), 17 | { headers: { "Content-Type": "application/json" } }, 18 | ) 19 | }) 20 | 21 | // To invoke: 22 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 23 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 24 | // --header 'Content-Type: application/json' \ 25 | // --data '{"name":"Functions"}' 26 | -------------------------------------------------------------------------------- /Tests/AuthTests/Resources/signup-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "859f402d-b3de-4105-a1b9-932836d9193b", 3 | "aud": "authenticated", 4 | "role": "authenticated", 5 | "email": "guilherme@grds.dev", 6 | "phone": "", 7 | "confirmation_sent_at": "2022-04-09T11:57:01.710600634Z", 8 | "app_metadata": { 9 | "provider": "email", 10 | "providers": [ 11 | "email" 12 | ] 13 | }, 14 | "user_metadata": {}, 15 | "identities": [ 16 | { 17 | "id": "859f402d-b3de-4105-a1b9-932836d9193b", 18 | "user_id": "859f402d-b3de-4105-a1b9-932836d9193b", 19 | "identity_id": "859f402d-b3de-4105-a1b9-932836d9193b", 20 | "identity_data": { 21 | "sub": "859f402d-b3de-4105-a1b9-932836d9193b" 22 | }, 23 | "provider": "email", 24 | "last_sign_in_at": "2022-04-09T11:23:45.899902Z", 25 | "created_at": "2022-04-09T11:23:45.899924Z", 26 | "updated_at": "2022-04-09T11:23:45.899926Z" 27 | } 28 | ], 29 | "created_at": "2022-04-09T11:23:45.874827Z", 30 | "updated_at": "2022-04-09T11:57:01.720803Z" 31 | } 32 | -------------------------------------------------------------------------------- /Examples/UserManagement/Supabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Supabase.swift 3 | // UserManagement 4 | // 5 | // Created by Guilherme Souza on 17/11/23. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import Supabase 11 | 12 | let supabase = SupabaseClient( 13 | supabaseURL: URL(string: "http://127.0.0.1:54321")!, 14 | supabaseKey: 15 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU", 16 | options: .init( 17 | global: .init(logger: AppLogger()) 18 | ) 19 | ) 20 | 21 | struct AppLogger: SupabaseLogger { 22 | let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "supabase") 23 | 24 | func log(message: SupabaseLogMessage) { 25 | switch message.level { 26 | case .verbose: 27 | logger.log(level: .info, "\(message.description)") 28 | case .debug: 29 | logger.log(level: .debug, "\(message.description)") 30 | case .warning, .error: 31 | logger.log(level: .error, "\(message.description)") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/Examples/Auth/AuthController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthController.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 22/12/22. 6 | // 7 | 8 | import Auth 9 | import SwiftUI 10 | 11 | @Observable 12 | @MainActor 13 | final class AuthController { 14 | var session: Session? 15 | var isPasswordRecoveryFlow: Bool = false 16 | 17 | var currentUserID: UUID { 18 | guard let id = session?.user.id else { 19 | preconditionFailure("Required session.") 20 | } 21 | 22 | return id 23 | } 24 | 25 | @ObservationIgnored 26 | private var observeAuthStateChangesTask: Task? 27 | 28 | init() { 29 | observeAuthStateChangesTask = Task { 30 | for await (event, session) in supabase.auth.authStateChanges { 31 | if [.initialSession, .signedIn, .signedOut].contains(event) { 32 | self.session = session 33 | } 34 | 35 | if event == .passwordRecovery { 36 | self.isPasswordRecoveryFlow = true 37 | } 38 | } 39 | } 40 | } 41 | 42 | deinit { 43 | observeAuthStateChangesTask?.cancel() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Examples/SlackClone/Supabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Supabase.swift 3 | // SlackClone 4 | // 5 | // Created by Guilherme Souza on 27/12/23. 6 | // 7 | 8 | import Foundation 9 | import Supabase 10 | 11 | let encoder: JSONEncoder = { 12 | let encoder = PostgrestClient.Configuration.jsonEncoder 13 | encoder.keyEncodingStrategy = .convertToSnakeCase 14 | return encoder 15 | }() 16 | 17 | let decoder: JSONDecoder = { 18 | let decoder = PostgrestClient.Configuration.jsonDecoder 19 | decoder.keyDecodingStrategy = .convertFromSnakeCase 20 | return decoder 21 | }() 22 | 23 | let supabase = SupabaseClient( 24 | supabaseURL: URL(string: "http://127.0.0.1:54321")!, 25 | supabaseKey: 26 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0", 27 | options: SupabaseClientOptions( 28 | db: .init(encoder: encoder, decoder: decoder), 29 | auth: .init(redirectToURL: URL(string: "com.supabase.slack-clone://login-callback")), 30 | global: SupabaseClientOptions.GlobalOptions(logger: SupaLogger()) 31 | ) 32 | ) 33 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Extracts parameters encoded in the URL both in the query and fragment. 4 | func extractParams(from url: URL) -> [String: String] { 5 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { 6 | return [:] 7 | } 8 | 9 | var result: [String: String] = [:] 10 | 11 | if let fragment = components.fragment { 12 | let items = extractParams(from: fragment) 13 | for item in items { 14 | result[item.name] = item.value 15 | } 16 | } 17 | 18 | if let items = components.queryItems { 19 | for item in items { 20 | result[item.name] = item.value 21 | } 22 | } 23 | 24 | return result 25 | } 26 | 27 | private func extractParams(from fragment: String) -> [URLQueryItem] { 28 | let components = 29 | fragment 30 | .split(separator: "&") 31 | .map { $0.split(separator: "=") } 32 | 33 | return 34 | components 35 | .compactMap { 36 | $0.count == 2 37 | ? URLQueryItem(name: String($0[0]), value: String($0[1])) 38 | : nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Supabase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/Dependencies.swift: -------------------------------------------------------------------------------- 1 | import ConcurrencyExtras 2 | import Foundation 3 | 4 | struct Dependencies: Sendable { 5 | var configuration: AuthClient.Configuration 6 | var http: any HTTPClientType 7 | var api: APIClient 8 | var codeVerifierStorage: CodeVerifierStorage 9 | var sessionStorage: SessionStorage 10 | var sessionManager: SessionManager 11 | 12 | var eventEmitter = AuthStateChangeEventEmitter() 13 | var date: @Sendable () -> Date = { Date() } 14 | 15 | var urlOpener: URLOpener = .live 16 | var pkce: PKCE = .live 17 | var logger: (any SupabaseLogger)? 18 | 19 | var encoder: JSONEncoder { configuration.encoder } 20 | var decoder: JSONDecoder { configuration.decoder } 21 | } 22 | 23 | extension Dependencies { 24 | static let instances = LockIsolated([AuthClientID: Dependencies]()) 25 | 26 | static subscript(_ id: AuthClientID) -> Dependencies { 27 | get { 28 | guard let instance = instances[id] else { 29 | fatalError("Dependencies not found for id: \(id)") 30 | } 31 | return instance 32 | } 33 | set { 34 | instances.withValue { $0[id] = newValue } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Helpers/Base64URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64URL.swift 3 | // Supabase 4 | // 5 | // Created by Claude on 06/10/25. 6 | // 7 | 8 | import Foundation 9 | 10 | package enum Base64URL { 11 | /// Decodes a base64url-encoded string to Data 12 | package static func decode(_ value: String) -> Data? { 13 | var base64 = value.replacingOccurrences(of: "-", with: "+") 14 | .replacingOccurrences(of: "_", with: "/") 15 | let length = Double(base64.lengthOfBytes(using: .utf8)) 16 | let requiredLength = 4 * ceil(length / 4.0) 17 | let paddingLength = requiredLength - length 18 | if paddingLength > 0 { 19 | let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) 20 | base64 = base64 + padding 21 | } 22 | return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) 23 | } 24 | 25 | /// Encodes Data to a base64url-encoded string 26 | package static func encode(_ data: Data) -> String { 27 | data.base64EncodedString() 28 | .replacingOccurrences(of: "+", with: "-") 29 | .replacingOccurrences(of: "/", with: "_") 30 | .replacingOccurrences(of: "=", with: "") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Examples/SlackClone/Dependencies.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dependencies.swift 3 | // SlackClone 4 | // 5 | // Created by Guilherme Souza on 04/01/24. 6 | // 7 | 8 | import Foundation 9 | import Supabase 10 | 11 | @MainActor 12 | class Dependencies { 13 | static let shared = Dependencies() 14 | 15 | let channel = ChannelStore.shared 16 | let users = UserStore.shared 17 | let messages = MessageStore.shared 18 | } 19 | 20 | struct User: Codable, Identifiable, Hashable { 21 | var id: UUID 22 | var username: String 23 | } 24 | 25 | struct AddChannel: Encodable { 26 | var slug: String 27 | var createdBy: UUID 28 | } 29 | 30 | struct Channel: Identifiable, Codable, Hashable { 31 | var id: Int 32 | var slug: String 33 | var insertedAt: Date 34 | } 35 | 36 | struct Message: Identifiable, Codable, Hashable { 37 | var id: Int 38 | var insertedAt: Date 39 | var message: String 40 | var user: User 41 | var channel: Channel 42 | } 43 | 44 | struct NewMessage: Codable { 45 | var message: String 46 | var userId: UUID 47 | let channelId: Int 48 | } 49 | 50 | struct UserPresence: Codable, Hashable { 51 | var userId: UUID 52 | var onlineAt: Date 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Storage/BucketOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct BucketOptions: Sendable { 4 | /// The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. Bu default, buckets are private. 5 | public var `public`: Bool 6 | /// Specifies the allowed mime types that this bucket can accept during upload. The default value is null, which allows files with all mime types to be uploaded. Each mime type specified can be a wildcard, e.g. image/*, or a specific mime type, e.g. image/png. 7 | public var fileSizeLimit: String? 8 | /// Specifies the max file size in bytes that can be uploaded to this bucket. The global file size limit takes precedence over this value. The default value is null, which doesn't set a per bucket file size limit. 9 | public var allowedMimeTypes: [String]? 10 | 11 | public init( 12 | public: Bool = false, 13 | fileSizeLimit: String? = nil, 14 | allowedMimeTypes: [String]? = nil 15 | ) { 16 | self.public = `public` 17 | self.fileSizeLimit = fileSizeLimit 18 | self.allowedMimeTypes = allowedMimeTypes 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Realtime/RealtimePostgresFilterValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimePostgresFilterValue.swift 3 | // Supabase 4 | // 5 | // Created by Lucas Abijmil on 19/02/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A value that can be used to filter Realtime changes in a channel. 11 | public protocol RealtimePostgresFilterValue { 12 | var rawValue: String { get } 13 | } 14 | 15 | extension String: RealtimePostgresFilterValue { 16 | public var rawValue: String { self } 17 | } 18 | 19 | extension Int: RealtimePostgresFilterValue { 20 | public var rawValue: String { "\(self)" } 21 | } 22 | 23 | extension Double: RealtimePostgresFilterValue { 24 | public var rawValue: String { "\(self)" } 25 | } 26 | 27 | extension Bool: RealtimePostgresFilterValue { 28 | public var rawValue: String { "\(self)" } 29 | } 30 | 31 | extension UUID: RealtimePostgresFilterValue { 32 | public var rawValue: String { uuidString } 33 | } 34 | 35 | extension Date: RealtimePostgresFilterValue { 36 | public var rawValue: String { 37 | let formatter = ISO8601DateFormatter() 38 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 39 | return formatter.string(from: self) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Examples/supabase/migrations/20221223094509_init.sql: -------------------------------------------------------------------------------- 1 | create table todos( 2 | id uuid default uuid_generate_v4() primary key not null, 3 | description text not null, 4 | is_complete boolean not null, 5 | created_at timestamptz default (now() at time zone 'utc'::text) not null, 6 | owner_id uuid references auth.users(id) not null 7 | ); 8 | 9 | alter table todos enable row level security; 10 | 11 | create policy "Allow access to owner only" on todos as permissive 12 | for all to authenticated 13 | using (auth.uid() = owner_id) 14 | with check (auth.uid() = owner_id); 15 | 16 | -- Storage 17 | create policy "Allow authenticated users to create buckets." on storage.buckets 18 | for insert to authenticated 19 | with check (true); 20 | 21 | create policy "Allow authenticated users to list buckets." on storage.buckets 22 | for select to authenticated 23 | using (true); 24 | 25 | create policy "Allow authenticated users to upload objects." on storage.objects 26 | for insert to authenticated 27 | with check (true); 28 | 29 | create policy "Allow authenticated users to list objects." on storage.objects 30 | for select to authenticated 31 | using (true); 32 | 33 | -------------------------------------------------------------------------------- /Sources/Auth/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 14/12/23. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | 11 | extension AuthClient.Configuration { 12 | /// The default JSONEncoder instance used by the ``AuthClient``. 13 | public static let jsonEncoder: JSONEncoder = { 14 | let encoder = JSONEncoder.supabase() 15 | encoder.keyEncodingStrategy = .convertToSnakeCase 16 | return encoder 17 | }() 18 | 19 | /// The default JSONDecoder instance used by the ``AuthClient``. 20 | public static let jsonDecoder: JSONDecoder = { 21 | let decoder = JSONDecoder.supabase() 22 | decoder.keyDecodingStrategy = .convertFromSnakeCase 23 | return decoder 24 | }() 25 | 26 | /// The default headers used by the ``AuthClient``. 27 | public static let defaultHeaders: [String: String] = [ 28 | "X-Client-Info": "auth-swift/\(version)" 29 | ] 30 | 31 | /// The default ``AuthFlowType`` used when initializing a ``AuthClient`` instance. 32 | public static let defaultFlowType: AuthFlowType = .pkce 33 | 34 | /// The default value when initializing a ``AuthClient`` instance. 35 | public static let defaultAutoRefreshToken: Bool = true 36 | } 37 | -------------------------------------------------------------------------------- /Examples/Examples/AddTodoListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddTodoListView.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 23/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AddTodoListView: View { 11 | @Binding var request: CreateTodoRequest 12 | let completion: (Result) -> Void 13 | 14 | var body: some View { 15 | Section { 16 | TextField("Description", text: $request.description) 17 | Button("Save") { 18 | Task { await saveButtonTapped() } 19 | } 20 | } 21 | } 22 | 23 | func saveButtonTapped() async { 24 | do { 25 | let createdTodo: Todo = try await supabase.from("todos") 26 | .insert(request, returning: .representation) 27 | .single() 28 | .execute() 29 | .value 30 | completion(.success(createdTodo)) 31 | } catch { 32 | completion(.failure(error)) 33 | } 34 | } 35 | } 36 | 37 | struct AddTodoListView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | AddTodoListView( 40 | request: .constant( 41 | .init( 42 | description: "", 43 | isComplete: false, 44 | ownerID: UUID() 45 | ) 46 | ) 47 | ) { _ in 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/StorageTests/StorageErrorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import Storage 4 | 5 | final class StorageErrorTests: XCTestCase { 6 | func testErrorInitialization() { 7 | let error = StorageError( 8 | statusCode: "404", 9 | message: "File not found", 10 | error: "NotFound" 11 | ) 12 | 13 | XCTAssertEqual(error.statusCode, "404") 14 | XCTAssertEqual(error.message, "File not found") 15 | XCTAssertEqual(error.error, "NotFound") 16 | } 17 | 18 | func testLocalizedError() { 19 | let error = StorageError( 20 | statusCode: "500", 21 | message: "Internal server error", 22 | error: nil 23 | ) 24 | 25 | XCTAssertEqual(error.errorDescription, "Internal server error") 26 | } 27 | 28 | func testDecoding() throws { 29 | let json = """ 30 | { 31 | "statusCode": "403", 32 | "message": "Unauthorized access", 33 | "error": "Forbidden" 34 | } 35 | """.data(using: .utf8)! 36 | 37 | let error = try JSONDecoder().decode(StorageError.self, from: json) 38 | 39 | XCTAssertEqual(error.statusCode, "403") 40 | XCTAssertEqual(error.message, "Unauthorized access") 41 | XCTAssertEqual(error.error, "Forbidden") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 22/05/24. 6 | // 7 | 8 | import Foundation 9 | import HTTPTypes 10 | 11 | let defaultAuthURL = URL(string: "http://localhost:9999")! 12 | let defaultExpiryMargin: TimeInterval = 30 13 | 14 | let autoRefreshTickDuration: TimeInterval = 30 15 | let autoRefreshTickThreshold = 3 16 | 17 | let defaultStorageKey = "supabase.auth.token" 18 | 19 | extension HTTPField.Name { 20 | static let apiVersionHeaderName = HTTPField.Name("X-Supabase-Api-Version")! 21 | } 22 | 23 | let apiVersions: [APIVersion.Name: APIVersion] = [ 24 | ._20240101: ._20240101 25 | ] 26 | 27 | struct APIVersion { 28 | let timestamp: Date 29 | let name: Name 30 | 31 | enum Name: String { 32 | case _20240101 = "2024-01-01" 33 | } 34 | 35 | static func date(for name: Name) -> Date { 36 | let formatter = ISO8601DateFormatter() 37 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 38 | return formatter.date(from: "\(name.rawValue)T00:00:00.0Z")! 39 | } 40 | } 41 | 42 | extension APIVersion { 43 | static let _20240101 = APIVersion( 44 | timestamp: APIVersion.date(for: ._20240101), 45 | name: ._20240101 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -- Seed data for Examples App 2 | -- This file contains sample data for testing the examples 3 | 4 | -- Note: In production, you would create users through the auth system 5 | -- For local testing, we can insert some sample data once users are created through the app 6 | 7 | -- Sample function to create test data after a user signs up 8 | create or replace function create_sample_data_for_user(user_id uuid) 9 | returns void 10 | language plpgsql 11 | security definer 12 | as $$ 13 | begin 14 | -- Create profile 15 | insert into profiles (id, username, full_name) 16 | values (user_id, 'demo_user', 'Demo User') 17 | on conflict (id) do nothing; 18 | 19 | -- Create sample todos 20 | insert into todos (description, is_complete, owner_id) 21 | values 22 | ('Welcome to Supabase Swift!', false, user_id), 23 | ('Try creating a new todo', false, user_id), 24 | ('Mark this todo as complete', false, user_id), 25 | ('Check out the Storage tab', false, user_id), 26 | ('Explore Realtime features', false, user_id); 27 | 28 | -- Create sample messages 29 | insert into messages (content, user_id, channel_id) 30 | values 31 | ('Welcome to the Examples app!', user_id, 'general'), 32 | ('This is a sample message', user_id, 'general'); 33 | end; 34 | $$; 35 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/PKCE.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | import Foundation 3 | 4 | struct PKCE { 5 | var generateCodeVerifier: @Sendable () -> String 6 | var generateCodeChallenge: @Sendable (_ codeVerifier: String) -> String 7 | } 8 | 9 | extension PKCE { 10 | static let live = PKCE( 11 | generateCodeVerifier: { 12 | let buffer = [UInt8].random(count: 64) 13 | return Data(buffer).pkceBase64EncodedString() 14 | }, 15 | generateCodeChallenge: { codeVerifier in 16 | guard let data = codeVerifier.data(using: .utf8) else { 17 | preconditionFailure("provided string should be utf8 encoded.") 18 | } 19 | 20 | var hasher = SHA256() 21 | hasher.update(data: data) 22 | let hashed = hasher.finalize() 23 | return Data(hashed).pkceBase64EncodedString() 24 | } 25 | ) 26 | } 27 | 28 | extension Data { 29 | // Returns a base64 encoded string, replacing reserved characters 30 | // as per the PKCE spec https://tools.ietf.org/html/rfc7636#section-4.2 31 | func pkceBase64EncodedString() -> String { 32 | base64EncodedString() 33 | .replacingOccurrences(of: "+", with: "-") 34 | .replacingOccurrences(of: "/", with: "_") 35 | .replacingOccurrences(of: "=", with: "") 36 | .trimmingCharacters(in: .whitespaces) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/AuthTests/MockHelpers.swift: -------------------------------------------------------------------------------- 1 | import ConcurrencyExtras 2 | import Foundation 3 | import TestHelpers 4 | 5 | @testable import Auth 6 | 7 | func json(named name: String) -> Data { 8 | let url = Bundle.module.url(forResource: name, withExtension: "json") 9 | return try! Data(contentsOf: url!) 10 | } 11 | 12 | extension Decodable { 13 | init(fromMockNamed name: String) { 14 | self = try! AuthClient.Configuration.jsonDecoder.decode(Self.self, from: json(named: name)) 15 | } 16 | } 17 | 18 | extension Dependencies { 19 | static var mock = Dependencies( 20 | configuration: AuthClient.Configuration( 21 | url: URL(string: "https://project-id.supabase.com")!, 22 | localStorage: InMemoryLocalStorage(), 23 | logger: nil 24 | ), 25 | http: HTTPClientMock(), 26 | api: APIClient(clientID: AuthClientID()), 27 | codeVerifierStorage: CodeVerifierStorage.mock, 28 | sessionStorage: SessionStorage.live(clientID: AuthClientID()), 29 | sessionManager: SessionManager.live(clientID: AuthClientID()) 30 | ) 31 | } 32 | 33 | extension CodeVerifierStorage { 34 | static var mock: CodeVerifierStorage { 35 | let code = LockIsolated(nil) 36 | 37 | return Self( 38 | get: { code.value }, 39 | set: { code.setValue($0) } 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Storage/SupabaseStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct StorageClientConfiguration: Sendable { 4 | public var url: URL 5 | public var headers: [String: String] 6 | public let encoder: JSONEncoder 7 | public let decoder: JSONDecoder 8 | public let session: StorageHTTPSession 9 | public let logger: (any SupabaseLogger)? 10 | public let useNewHostname: Bool 11 | 12 | public init( 13 | url: URL, 14 | headers: [String: String], 15 | encoder: JSONEncoder = .defaultStorageEncoder, 16 | decoder: JSONDecoder = .defaultStorageDecoder, 17 | session: StorageHTTPSession = .init(), 18 | logger: (any SupabaseLogger)? = nil, 19 | useNewHostname: Bool = false 20 | ) { 21 | self.url = url 22 | self.headers = headers 23 | self.encoder = encoder 24 | self.decoder = decoder 25 | self.session = session 26 | self.logger = logger 27 | self.useNewHostname = useNewHostname 28 | } 29 | } 30 | 31 | public class SupabaseStorageClient: StorageBucketApi, @unchecked Sendable { 32 | /// Perform file operation in a bucket. 33 | /// - Parameter id: The bucket id to operate on. 34 | /// - Returns: StorageFileApi object 35 | public func from(_ id: String) -> StorageFileApi { 36 | StorageFileApi(bucketId: id, configuration: configuration) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Examples/Examples/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Examples/SlackClone/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Examples/UserManagement/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/AuthTests/AuthClientMultipleInstancesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthClientMultipleInstancesTests.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 05/07/24. 6 | // 7 | 8 | import TestHelpers 9 | import XCTest 10 | 11 | @testable import Auth 12 | 13 | final class AuthClientMultipleInstancesTests: XCTestCase { 14 | func testMultipleAuthClientInstances() { 15 | let url = URL(string: "http://localhost:54321/auth")! 16 | 17 | let client1Storage = InMemoryLocalStorage() 18 | let client2Storage = InMemoryLocalStorage() 19 | 20 | let client1 = AuthClient( 21 | configuration: AuthClient.Configuration( 22 | url: url, 23 | localStorage: client1Storage, 24 | logger: nil 25 | ) 26 | ) 27 | 28 | let client2 = AuthClient( 29 | configuration: AuthClient.Configuration( 30 | url: url, 31 | localStorage: client2Storage, 32 | logger: nil 33 | ) 34 | ) 35 | 36 | XCTAssertNotEqual(client1.clientID, client2.clientID) 37 | 38 | XCTAssertIdentical( 39 | Dependencies[client1.clientID].configuration.localStorage as? InMemoryLocalStorage, 40 | client1Storage 41 | ) 42 | XCTAssertIdentical( 43 | Dependencies[client2.clientID].configuration.localStorage as? InMemoryLocalStorage, 44 | client2Storage 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/AuthTests/Resources/anonymous-sign-in-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token" : "eyJhbGciOiJIUzI1NiIsImtpZCI6ImpIaU1GZmtNTzRGdVROdXUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzExOTk0NzEzLCJpYXQiOjE3MTE5OTExMTMsImlzcyI6Imh0dHBzOi8vYWp5YWdzaHV6bnV2anFoampmdG8uc3VwYWJhc2UuY28vYXV0aC92MSIsInN1YiI6ImJiZmE5MjU0LWM1ZDEtNGNmZi1iYTc2LTU2YmYwM2IwNWEwMSIsImVtYWlsIjoiIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnt9LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJhbm9ueW1vdXMiLCJ0aW1lc3RhbXAiOjE3MTE5OTExMTN9XSwic2Vzc2lvbl9pZCI6ImMyODlmYTcwLWIzYWUtNDI1Yi05MDQxLWUyZjVhNzBlZTcyYSIsImlzX2Fub255bW91cyI6dHJ1ZX0.whBzmyMv3-AQSaiY6Fi-v_G68Q8oULhB7axImj9qOdw", 3 | "expires_at" : 1711994713, 4 | "expires_in" : 3600, 5 | "refresh_token" : "0xS9iJUWdXnWlCJtFiXk5A", 6 | "token_type" : "bearer", 7 | "user" : { 8 | "app_metadata" : { 9 | 10 | }, 11 | "aud" : "authenticated", 12 | "created_at" : "2024-04-01T17:05:13.013312Z", 13 | "email" : "", 14 | "id" : "bbfa9254-c5d1-4cff-ba76-56bf03b05a01", 15 | "identities" : [ 16 | 17 | ], 18 | "is_anonymous" : true, 19 | "last_sign_in_at" : "2024-04-01T17:05:13.018294975Z", 20 | "phone" : "", 21 | "role" : "authenticated", 22 | "updated_at" : "2024-04-01T17:05:13.022041Z", 23 | "user_metadata" : { 24 | 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Examples/UserManagement/AvatarImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AvatarImage.swift 3 | // UserManagement 4 | // 5 | // Created by Guilherme Souza on 17/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(UIKit) 11 | typealias PlatformImage = UIImage 12 | extension Image { 13 | init(platformImage: PlatformImage) { 14 | self.init(uiImage: platformImage) 15 | } 16 | } 17 | 18 | #elseif canImport(AppKit) 19 | typealias PlatformImage = NSImage 20 | extension Image { 21 | init(platformImage: PlatformImage) { 22 | self.init(nsImage: platformImage) 23 | } 24 | } 25 | #endif 26 | 27 | struct AvatarImage: Transferable, Equatable { 28 | let image: Image 29 | let data: Data 30 | 31 | static var transferRepresentation: some TransferRepresentation { 32 | DataRepresentation(importedContentType: .image) { data in 33 | guard let image = await AvatarImage(data: data) else { 34 | throw TransferError.importFailed 35 | } 36 | 37 | return image 38 | } 39 | } 40 | } 41 | 42 | extension AvatarImage { 43 | @MainActor 44 | init?(data: Data) { 45 | guard let uiImage = PlatformImage(data: data) else { 46 | return nil 47 | } 48 | 49 | let image = Image(platformImage: uiImage) 50 | self.init(image: image, data: data) 51 | } 52 | } 53 | 54 | enum TransferError: Error { 55 | case importFailed 56 | } 57 | -------------------------------------------------------------------------------- /Tests/AuthTests/Resources/local-storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "supabase.auth.token" : { 3 | "accessToken" : "accesstoken", 4 | "expiresAt" : 1711977907, 5 | "expiresIn" : 120, 6 | "refreshToken" : "refreshtoken", 7 | "tokenType" : "bearer", 8 | "user" : { 9 | "appMetadata" : { 10 | "provider" : "email", 11 | "providers" : [ 12 | "email" 13 | ] 14 | }, 15 | "aud" : "authenticated", 16 | "confirmationSentAt" : 671198221, 17 | "createdAt" : 671198221, 18 | "email" : "johndoe@supabsae.com", 19 | "id" : "859F402D-B3DE-4105-A1B9-932836D9193B", 20 | "identities" : [ 21 | { 22 | "createdAt" : 671198221, 23 | "id" : "859f402d-b3de-4105-a1b9-932836d9193b", 24 | "identityData" : { 25 | "sub" : "859f402d-b3de-4105-a1b9-932836d9193b" 26 | }, 27 | "identityId" : "859F402D-B3DE-4105-A1B9-932836D9193B", 28 | "lastSignInAt" : 671198221, 29 | "provider" : "email", 30 | "updatedAt" : 671198221, 31 | "userId" : "859F402D-B3DE-4105-A1B9-932836D9193B" 32 | } 33 | ], 34 | "isAnonymous" : false, 35 | "phone" : "", 36 | "role" : "authenticated", 37 | "updatedAt" : 671198221, 38 | "userMetadata" : { 39 | "referrer_id" : null 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Examples/Examples/Auth/GoogleSignInSDKFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoogleSignInSDKFlow.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 05/03/24. 6 | // 7 | 8 | import GoogleSignIn 9 | import GoogleSignInSwift 10 | import Supabase 11 | import SwiftUI 12 | 13 | @MainActor 14 | struct GoogleSignInSDKFlow: View { 15 | var body: some View { 16 | GoogleSignInButton(action: handleSignIn) 17 | } 18 | 19 | func handleSignIn() { 20 | Task { 21 | do { 22 | let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: root) 23 | 24 | guard let idToken = result.user.idToken?.tokenString else { 25 | debug("No 'idToken' returned by GIDSignIn call.") 26 | return 27 | } 28 | 29 | try await supabase.auth.signInWithIdToken( 30 | credentials: OpenIDConnectCredentials( 31 | provider: .google, 32 | idToken: idToken 33 | ) 34 | ) 35 | } catch { 36 | debug("GoogleSignIn failed: \(error)") 37 | } 38 | } 39 | } 40 | 41 | #if canImport(UIKit) 42 | var root: UIViewController { 43 | UIApplication.shared.firstKeyWindow?.rootViewController ?? UIViewController() 44 | } 45 | #else 46 | var root: NSWindow { 47 | NSApplication.shared.keyWindow ?? NSWindow() 48 | } 49 | #endif 50 | } 51 | 52 | #Preview { 53 | GoogleSignInSDKFlow() 54 | } 55 | -------------------------------------------------------------------------------- /Tests/AuthTests/ExtractParamsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtractParamsTests.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 23/12/23. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import Auth 11 | 12 | final class ExtractParamsTests: XCTestCase { 13 | func testExtractParamsInQuery() { 14 | let code = UUID().uuidString 15 | let url = URL(string: "io.supabase.flutterquickstart://login-callback/?code=\(code)")! 16 | let params = extractParams(from: url) 17 | XCTAssertEqual(params, ["code": code]) 18 | } 19 | 20 | func testExtractParamsInFragment() { 21 | let code = UUID().uuidString 22 | let url = URL(string: "io.supabase.flutterquickstart://login-callback/#code=\(code)")! 23 | let params = extractParams(from: url) 24 | XCTAssertEqual(params, ["code": code]) 25 | } 26 | 27 | func testExtractParamsInBothFragmentAndQuery() { 28 | let code = UUID().uuidString 29 | let url = URL( 30 | string: "io.supabase.flutterquickstart://login-callback/?code=\(code)#message=abc")! 31 | let params = extractParams(from: url) 32 | XCTAssertEqual(params, ["code": code, "message": "abc"]) 33 | } 34 | 35 | func testExtractParamsQueryTakesPrecedence() { 36 | let url = URL(string: "io.supabase.flutterquickstart://login-callback/?code=123#code=abc")! 37 | let params = extractParams(from: url) 38 | XCTAssertEqual(params, ["code": "123"]) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Examples/Examples/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | 14 | 15 | 16 | CFBundleTypeRole 17 | Editor 18 | CFBundleURLSchemes 19 | 20 | {{ DOT_REVERSED_IOS_CLIENT_ID }} 21 | 22 | 23 | 24 | CFBundleURLSchemes 25 | 26 | fb{{ FACEBOOK APP ID }} 27 | 28 | 29 | 30 | GIDClientID 31 | {{ YOUR_IOS_CLIENT_ID }} 32 | GIDServerClientID 33 | {{ YOUR_SERVER_CLIENT_ID }} 34 | FacebookAppID 35 | {{ FACEBOOK APP ID }} 36 | FacebookClientToken 37 | {{ FACEBOOK CLIENT TOKEN }} 38 | FacebookDisplayName 39 | Examples 40 | LSApplicationQueriesSchemes 41 | 42 | fbapi 43 | fb-messenger-share-api 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Sources/Helpers/Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 20/01/25. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | import XCTestDynamicOverlay 11 | 12 | extension JSONDecoder { 13 | /// Default `JSONDecoder` for decoding types from Supabase. 14 | package static func supabase() -> JSONDecoder { 15 | let decoder = JSONDecoder() 16 | decoder.dateDecodingStrategy = .custom { decoder in 17 | let container = try decoder.singleValueContainer() 18 | let string = try container.decode(String.self) 19 | 20 | if let date = string.date { 21 | return date 22 | } 23 | 24 | throw DecodingError.dataCorruptedError( 25 | in: container, 26 | debugDescription: "Invalid date format: \(string)" 27 | ) 28 | } 29 | return decoder 30 | } 31 | } 32 | extension JSONEncoder { 33 | /// Default `JSONEncoder` for encoding types to Supabase. 34 | package static func supabase() -> JSONEncoder { 35 | let encoder = JSONEncoder() 36 | encoder.dateEncodingStrategy = .custom { date, encoder in 37 | var container = encoder.singleValueContainer() 38 | let string = date.iso8601String 39 | try container.encode(string) 40 | } 41 | 42 | #if DEBUG 43 | if isTesting { 44 | encoder.outputFormatting = [.sortedKeys] 45 | } 46 | #endif 47 | 48 | return encoder 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Examples/Examples/Shared/GitHubSourceLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHubSourceLink.swift 3 | // Examples 4 | // 5 | // Helper for generating GitHub source code links 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct GitHubSourceLink { 12 | static let baseURL = URL( 13 | string: "https://github.com/supabase/supabase-swift/blob/main" 14 | )! 15 | 16 | static func url(for file: String = #file) -> URL { 17 | let paths = file.split(separator: "/") 18 | 19 | guard let rootIndex = paths.firstIndex(where: { $0 == "Examples" }) else { 20 | return baseURL 21 | } 22 | 23 | let relativePath = paths[rootIndex...].joined(separator: "/") 24 | return baseURL.appendingPathComponent(relativePath) 25 | } 26 | } 27 | 28 | struct GitHubSourceLinkViewModifier: ViewModifier { 29 | @Environment(\.openURL) var openURL 30 | 31 | let file: String 32 | 33 | func body(content: Content) -> some View { 34 | content 35 | .toolbar { 36 | ToolbarItem(placement: .primaryAction) { 37 | Button { 38 | openURL(GitHubSourceLink.url(for: file)) 39 | } label: { 40 | Label("View Source", systemImage: "chevron.left.forwardslash.chevron.right") 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | extension View { 48 | func gitHubSourceLink(for file: String = #file) -> some View { 49 | modifier(GitHubSourceLinkViewModifier(file: file)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/TestHelpers/MockExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockExtensions.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 21/01/25. 6 | // 7 | 8 | import Mocker 9 | import Foundation 10 | import InlineSnapshotTesting 11 | 12 | extension Mock { 13 | package func snapshotRequest( 14 | message: @autoclosure () -> String = "", 15 | record isRecording: SnapshotTestingConfiguration.Record? = nil, 16 | timeout: TimeInterval = 5, 17 | syntaxDescriptor: InlineSnapshotSyntaxDescriptor = InlineSnapshotSyntaxDescriptor(), 18 | matches expected: (() -> String)? = nil, 19 | fileID: StaticString = #fileID, 20 | file filePath: StaticString = #filePath, 21 | function: StaticString = #function, 22 | line: UInt = #line, 23 | column: UInt = #column 24 | ) -> Self { 25 | #if os(Linux) || os(Android) 26 | // non-Darwin curl snapshots have a different Content-Length than expected 27 | return self 28 | #endif 29 | var copy = self 30 | copy.onRequestHandler = OnRequestHandler { 31 | assertInlineSnapshot( 32 | of: $0, 33 | as: ._curl, 34 | record: isRecording, 35 | timeout: timeout, 36 | syntaxDescriptor: syntaxDescriptor, 37 | matches: expected, 38 | fileID: fileID, 39 | file: filePath, 40 | function: function, 41 | line: line, 42 | column: column 43 | ) 44 | } 45 | return copy 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/PostgRESTTests/PostgrestFilterValueTests.swift: -------------------------------------------------------------------------------- 1 | import PostgREST 2 | import XCTest 3 | 4 | final class PostgrestFilterValue: XCTestCase { 5 | func testArray() { 6 | let array = ["is:online", "faction:red"] 7 | let queryValue = array.rawValue 8 | XCTAssertEqual(queryValue, "{is:online,faction:red}") 9 | } 10 | 11 | func testAnyJSON() { 12 | XCTAssertEqual( 13 | AnyJSON.array(["is:online", "faction:red"]).rawValue, 14 | "{is:online,faction:red}" 15 | ) 16 | XCTAssertEqual( 17 | AnyJSON.object(["postalcode": 90210]).rawValue, 18 | "{\"postalcode\":90210}" 19 | ) 20 | XCTAssertEqual(AnyJSON.string("string").rawValue, "string") 21 | XCTAssertEqual(AnyJSON.double(3.14).rawValue, "3.14") 22 | XCTAssertEqual(AnyJSON.integer(3).rawValue, "3") 23 | XCTAssertEqual(AnyJSON.bool(true).rawValue, "true") 24 | XCTAssertEqual(AnyJSON.null.rawValue, "NULL") 25 | } 26 | 27 | func testOptional() { 28 | XCTAssertEqual(Optional.some([1, 2]).rawValue, "{1,2}") 29 | XCTAssertEqual(Optional<[Int]>.none.rawValue, "NULL") 30 | } 31 | 32 | func testUUID() { 33 | XCTAssertEqual( 34 | UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!.rawValue, 35 | "E621E1F8-C36C-495A-93FC-0C247A3E6E5F") 36 | } 37 | 38 | func testDate() { 39 | XCTAssertEqual( 40 | Date(timeIntervalSince1970: 1_737_465_985).rawValue, 41 | "2025-01-21T13:26:25.000Z" 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Realtime/RealtimePostgresFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimePostgresFilter.swift 3 | // Supabase 4 | // 5 | // Created by Lucas Abijmil on 19/02/2025. 6 | // 7 | 8 | /// A filter that can be used in Realtime. 9 | public enum RealtimePostgresFilter { 10 | case eq(_ column: String, value: any RealtimePostgresFilterValue) 11 | case neq(_ column: String, value: any RealtimePostgresFilterValue) 12 | case gt(_ column: String, value: any RealtimePostgresFilterValue) 13 | case gte(_ column: String, value: any RealtimePostgresFilterValue) 14 | case lt(_ column: String, value: any RealtimePostgresFilterValue) 15 | case lte(_ column: String, value: any RealtimePostgresFilterValue) 16 | case `in`(_ column: String, values: [any RealtimePostgresFilterValue]) 17 | 18 | var value: String { 19 | switch self { 20 | case let .eq(column, value): 21 | return "\(column)=eq.\(value.rawValue)" 22 | case let .neq(column, value): 23 | return "\(column)=neq.\(value.rawValue)" 24 | case let .gt(column, value): 25 | return "\(column)=gt.\(value.rawValue)" 26 | case let .gte(column, value): 27 | return "\(column)=gte.\(value.rawValue)" 28 | case let .lt(column, value): 29 | return "\(column)=lt.\(value.rawValue)" 30 | case let .lte(column, value): 31 | return "\(column)=lte.\(value.rawValue)" 32 | case let .in(column, values): 33 | return "\(column)=in.(\(values.map(\.rawValue).joined(separator: ",")))" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Helpers/TaskLocalHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskLocalHelpers.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 29/05/24. 6 | // 7 | 8 | import Foundation 9 | 10 | #if compiler(>=6.0) 11 | extension TaskLocal where Value == JSONObject { 12 | @discardableResult 13 | @inlinable package final func withValue( 14 | merging valueDuringOperation: Value, 15 | operation: () async throws -> R, 16 | isolation: isolated (any Actor)? = #isolation, 17 | file: String = #fileID, 18 | line: UInt = #line 19 | ) async rethrows -> R { 20 | let currentValue = wrappedValue 21 | return try await withValue( 22 | currentValue.merging(valueDuringOperation) { _, new in new }, 23 | operation: operation, 24 | isolation: isolation, 25 | file: file, 26 | line: line 27 | ) 28 | } 29 | } 30 | #else 31 | extension TaskLocal where Value == JSONObject { 32 | @_unsafeInheritExecutor 33 | @discardableResult 34 | @inlinable package final func withValue( 35 | merging valueDuringOperation: Value, 36 | operation: () async throws -> R, 37 | file: String = #fileID, 38 | line: UInt = #line 39 | ) async rethrows -> R { 40 | let currentValue = wrappedValue 41 | return try await withValue( 42 | currentValue.merging(valueDuringOperation) { _, new in new }, 43 | operation: operation, 44 | file: file, 45 | line: line 46 | ) 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /Tests/AuthTests/Resources/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "859f402d-b3de-4105-a1b9-932836d9193b", 3 | "aud": "authenticated", 4 | "role": "authenticated", 5 | "email": "guilherme@grds.dev", 6 | "phone": "", 7 | "confirmation_sent_at": "2022-04-09T11:57:01.710600634Z", 8 | "app_metadata": { 9 | "provider": "email", 10 | "providers": [ 11 | "email" 12 | ] 13 | }, 14 | "user_metadata": { 15 | "referrer_id": null 16 | }, 17 | "identities": [ 18 | { 19 | "id": "859f402d-b3de-4105-a1b9-932836d9193b", 20 | "user_id": "859f402d-b3de-4105-a1b9-932836d9193b", 21 | "identity_id": "859f402d-b3de-4105-a1b9-932836d9193b", 22 | "identity_data": { 23 | "sub": "859f402d-b3de-4105-a1b9-932836d9193b" 24 | }, 25 | "provider": "email", 26 | "last_sign_in_at": "2022-04-09T11:23:45Z", 27 | "created_at": "2022-04-09T11:23:45.899924Z", 28 | "updated_at": "2022-04-09T11:23:45.899926Z" 29 | }, 30 | { 31 | "id": "6c69f399-be1b-467a-9c53-d3753abdd2df", 32 | "user_id": "6c69f399-be1b-467a-9c53-d3753abdd2df", 33 | "identity_id": "6c69f399-be1b-467a-9c53-d3753abdd2df", 34 | "identity_data": { 35 | "sub": "6c69f399-be1b-467a-9c53-d3753abdd2df" 36 | }, 37 | "provider": "github", 38 | "created_at": "2022-04-09T11:23:45.899924Z", 39 | "updated_at": "2022-04-09T11:23:45.899926Z" 40 | } 41 | ], 42 | "created_at": "2022-04-09T11:23:45.874827Z", 43 | "updated_at": "2022-04-09T11:57:01.720803Z" 44 | } 45 | -------------------------------------------------------------------------------- /Examples/SlackClone/ChannelListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelListView.swift 3 | // SlackClone 4 | // 5 | // Created by Guilherme Souza on 27/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChannelListView: View { 11 | @Bindable var store = Dependencies.shared.channel 12 | @Binding var channel: Channel? 13 | 14 | @State private var addChannelPresented = false 15 | @State private var newChannelName = "" 16 | 17 | var body: some View { 18 | List(store.channels, selection: $channel) { channel in 19 | NavigationLink(channel.slug, value: channel) 20 | } 21 | .toolbar { 22 | ToolbarItem(placement: .primaryAction) { 23 | Button("Add Channel") { 24 | addChannelPresented = true 25 | } 26 | .popover(isPresented: $addChannelPresented) { 27 | addChannelView 28 | } 29 | } 30 | ToolbarItem { 31 | Button("Log out") { 32 | Task { 33 | try? await supabase.auth.signOut() 34 | } 35 | } 36 | } 37 | } 38 | .toast(state: $store.toast) 39 | } 40 | 41 | private var addChannelView: some View { 42 | Form { 43 | Section { 44 | TextField("New channel name", text: $newChannelName) 45 | } 46 | 47 | Section { 48 | Button("Add") { 49 | Task { 50 | await store.addChannel(newChannelName) 51 | addChannelPresented = false 52 | } 53 | } 54 | } 55 | } 56 | #if os(macOS) 57 | .padding() 58 | #endif 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Examples/Examples/ActionState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionState.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 15/12/23. 6 | // 7 | 8 | import CasePaths 9 | import Foundation 10 | import SwiftUI 11 | 12 | @CasePathable 13 | enum ActionState { 14 | case idle 15 | case inFlight 16 | case result(Result) 17 | 18 | var success: Success? { 19 | if case .result(.success(let success)) = self { return success } 20 | return nil 21 | } 22 | } 23 | 24 | struct ActionStateView: View { 25 | @Binding var state: ActionState 26 | 27 | let action: () async throws -> Success 28 | @ViewBuilder var content: (Success) -> SuccessContent 29 | 30 | var body: some View { 31 | Group { 32 | switch state { 33 | case .idle: 34 | Color.clear 35 | case .inFlight: 36 | ProgressView() 37 | case .result(.success(let value)): 38 | content(value) 39 | case .result(.failure(let error)): 40 | VStack { 41 | ErrorText(error) 42 | Button("Retry") { 43 | Task { await load() } 44 | } 45 | } 46 | } 47 | } 48 | .task { 49 | await load() 50 | } 51 | } 52 | 53 | @MainActor 54 | private func load() async { 55 | state = .inFlight 56 | do { 57 | let value = try await action() 58 | state = .result(.success(value)) 59 | } catch { 60 | state = .result(.failure(error)) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Examples/SlackClone/AuthView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthView.swift 3 | // SlackClone 4 | // 5 | // Created by Guilherme Souza on 27/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @Observable 11 | @MainActor 12 | final class AuthViewModel { 13 | var email = "" 14 | var toast: ToastState? 15 | 16 | func signInButtonTapped() async { 17 | do { 18 | try await supabase.auth.signInWithOTP(email: email) 19 | toast = ToastState(status: .success, title: "Check your inbox.") 20 | 21 | try? await Task.sleep(for: .seconds(1)) 22 | 23 | #if os(macOS) 24 | NSWorkspace.shared.open(URL(string: "http://127.0.0.1:54324")!) 25 | #else 26 | await UIApplication.shared.open(URL(string: "http://127.0.0.1:54324")!) 27 | #endif 28 | } catch { 29 | toast = ToastState(status: .error, title: "Error", description: error.localizedDescription) 30 | } 31 | } 32 | } 33 | 34 | struct AuthView: View { 35 | @Bindable var model = AuthViewModel() 36 | 37 | var body: some View { 38 | VStack { 39 | VStack { 40 | TextField("Email", text: $model.email) 41 | #if os(iOS) 42 | .textInputAutocapitalization(.never) 43 | .keyboardType(.emailAddress) 44 | #endif 45 | .textContentType(.emailAddress) 46 | .autocorrectionDisabled() 47 | } 48 | Button("Sign in with Magic Link") { 49 | Task { await model.signInButtonTapped() } 50 | } 51 | } 52 | .padding() 53 | .toast(state: $model.toast) 54 | } 55 | } 56 | 57 | #Preview { 58 | AuthView() 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Auth/Internal/CodeVerifierStorage.swift: -------------------------------------------------------------------------------- 1 | import ConcurrencyExtras 2 | import Foundation 3 | 4 | struct CodeVerifierStorage: Sendable { 5 | var get: @Sendable () -> String? 6 | var set: @Sendable (_ code: String?) -> Void 7 | } 8 | 9 | extension CodeVerifierStorage { 10 | static func live(clientID: AuthClientID) -> Self { 11 | var configuration: AuthClient.Configuration { Dependencies[clientID].configuration } 12 | var key: String { "\(configuration.storageKey ?? defaultStorageKey)-code-verifier" } 13 | 14 | return Self( 15 | get: { 16 | do { 17 | guard let data = try configuration.localStorage.retrieve(key: key) else { 18 | configuration.logger?.debug("Code verifier not found.") 19 | return nil 20 | } 21 | return String(decoding: data, as: UTF8.self) 22 | } catch { 23 | configuration.logger?.error("Failure loading code verifier: \(error.localizedDescription)") 24 | return nil 25 | } 26 | }, 27 | set: { code in 28 | do { 29 | if let code, let data = code.data(using: .utf8) { 30 | try configuration.localStorage.store(key: key, value: data) 31 | } else if code == nil { 32 | try configuration.localStorage.remove(key: key) 33 | } else { 34 | configuration.logger?.error("Code verifier is not a valid UTF8 string.") 35 | } 36 | } catch { 37 | configuration.logger?.error("Failure storing code verifier: \(error.localizedDescription)") 38 | } 39 | } 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/PostgRESTTests/PostgresQueryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostgrestQueryTests.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 21/01/25. 6 | // 7 | 8 | import InlineSnapshotTesting 9 | import Mocker 10 | import PostgREST 11 | import TestHelpers 12 | import XCTest 13 | 14 | #if canImport(FoundationNetworking) 15 | import FoundationNetworking 16 | #endif 17 | 18 | class PostgrestQueryTests: XCTestCase { 19 | let url = URL(string: "http://localhost:54321/rest/v1")! 20 | 21 | let sessionConfiguration: URLSessionConfiguration = { 22 | let configuration = URLSessionConfiguration.default 23 | configuration.protocolClasses = [MockingURLProtocol.self] 24 | return configuration 25 | }() 26 | 27 | lazy var session = URLSession(configuration: sessionConfiguration) 28 | 29 | lazy var sut = PostgrestClient( 30 | url: url, 31 | headers: [ 32 | "apikey": 33 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" 34 | ], 35 | logger: nil, 36 | fetch: { 37 | try await self.session.data(for: $0) 38 | }, 39 | encoder: { 40 | let encoder = PostgrestClient.Configuration.jsonEncoder 41 | encoder.outputFormatting = [.sortedKeys] 42 | return encoder 43 | }() 44 | ) 45 | 46 | struct User: Codable { 47 | let id: Int 48 | let username: String 49 | } 50 | 51 | struct Country: Decodable { 52 | let name: String 53 | let cities: [City] 54 | 55 | struct City: Decodable { 56 | let name: String 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Helpers/_Clock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _Clock.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 08/01/25. 6 | // 7 | 8 | import Clocks 9 | import ConcurrencyExtras 10 | import Foundation 11 | 12 | package protocol _Clock: Sendable { 13 | func sleep(for duration: TimeInterval) async throws 14 | } 15 | 16 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 17 | extension ContinuousClock: _Clock { 18 | package func sleep(for duration: TimeInterval) async throws { 19 | try await sleep(for: .seconds(duration)) 20 | } 21 | } 22 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 23 | extension TestClock: _Clock { 24 | package func sleep(for duration: TimeInterval) async throws { 25 | try await sleep(for: .seconds(duration)) 26 | } 27 | } 28 | 29 | /// `_Clock` used on platforms where ``Clock`` protocol isn't available. 30 | struct FallbackClock: _Clock { 31 | func sleep(for duration: TimeInterval) async throws { 32 | try await Task.sleep(nanoseconds: NSEC_PER_SEC * UInt64(duration)) 33 | } 34 | } 35 | 36 | // Resolves clock instance based on platform availability. 37 | let _resolveClock: @Sendable () -> any _Clock = { 38 | if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { 39 | ContinuousClock() 40 | } else { 41 | FallbackClock() 42 | } 43 | } 44 | 45 | private let __clock = LockIsolated(_resolveClock()) 46 | 47 | #if DEBUG 48 | package var _clock: any _Clock { 49 | get { 50 | __clock.value 51 | } 52 | set { 53 | __clock.setValue(newValue) 54 | } 55 | } 56 | #else 57 | package var _clock: any _Clock { 58 | __clock.value 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Examples/SlackClone/AppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppView.swift 3 | // SlackClone 4 | // 5 | // Created by Guilherme Souza on 27/12/23. 6 | // 7 | 8 | import OSLog 9 | import Supabase 10 | import SwiftUI 11 | 12 | @Observable 13 | @MainActor 14 | final class AppViewModel { 15 | var session: Session? 16 | var selectedChannel: Channel? 17 | 18 | var realtimeConnectionStatus: RealtimeClientStatus? 19 | 20 | init() { 21 | Task { 22 | for await (event, session) in supabase.auth.authStateChanges { 23 | Logger.main.debug("AuthStateChange: \(event.rawValue)") 24 | guard [.signedIn, .signedOut, .initialSession, .tokenRefreshed].contains(event) else { 25 | return 26 | } 27 | self.session = session 28 | 29 | if session == nil { 30 | for subscription in supabase.channels { 31 | await subscription.unsubscribe() 32 | } 33 | } 34 | } 35 | } 36 | 37 | Task { 38 | for await status in supabase.realtimeV2.statusChange { 39 | realtimeConnectionStatus = status 40 | } 41 | } 42 | } 43 | } 44 | 45 | struct AppView: View { 46 | @Bindable var model: AppViewModel 47 | @State var logPresented = false 48 | 49 | @ViewBuilder 50 | var body: some View { 51 | if model.session != nil { 52 | NavigationSplitView { 53 | ChannelListView(channel: $model.selectedChannel) 54 | } detail: { 55 | if let channel = model.selectedChannel { 56 | MessagesView(channel: channel).id(channel.id) 57 | } 58 | } 59 | } else { 60 | AuthView() 61 | } 62 | } 63 | } 64 | 65 | #Preview { 66 | AppView(model: AppViewModel()) 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Realtime/PushV2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PushV2.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 02/01/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents the different status of a push 11 | public enum PushStatus: String, Sendable { 12 | case ok 13 | case error 14 | case timeout 15 | } 16 | 17 | @MainActor 18 | final class PushV2 { 19 | private weak var channel: (any RealtimeChannelProtocol)? 20 | let message: RealtimeMessageV2 21 | 22 | private var receivedContinuation: CheckedContinuation? 23 | 24 | init(channel: (any RealtimeChannelProtocol)?, message: RealtimeMessageV2) { 25 | self.channel = channel 26 | self.message = message 27 | } 28 | 29 | func send() async -> PushStatus { 30 | guard let channel = channel else { 31 | return .error 32 | } 33 | 34 | channel.socket.push(message) 35 | 36 | if !channel.config.broadcast.acknowledgeBroadcasts { 37 | // channel was configured with `ack = false`, 38 | // don't wait for a response and return `ok`. 39 | return .ok 40 | } 41 | 42 | do { 43 | return try await withTimeout(interval: channel.socket.options.timeoutInterval) { 44 | await withCheckedContinuation { continuation in 45 | self.receivedContinuation = continuation 46 | } 47 | } 48 | } catch is TimeoutError { 49 | channel.logger?.debug("Push timed out.") 50 | return .timeout 51 | } catch { 52 | channel.logger?.error("Error sending push: \(error.localizedDescription)") 53 | return .error 54 | } 55 | } 56 | 57 | func didReceive(status: PushStatus) { 58 | receivedContinuation?.resume(returning: status) 59 | receivedContinuation = nil 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/HelpersTests/EventEmitterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventEmitterTests.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 15/10/24. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Helpers 10 | import XCTest 11 | 12 | final class EventEmitterTests: XCTestCase { 13 | 14 | func testBasics() { 15 | let sut = EventEmitter(initialEvent: "0") 16 | XCTAssertTrue(sut.emitsLastEventWhenAttaching) 17 | 18 | XCTAssertEqual(sut.lastEvent, "0") 19 | 20 | let receivedEvents = LockIsolated<[String]>([]) 21 | 22 | let tokenA = sut.attach { value in 23 | receivedEvents.withValue { $0.append("a" + value) } 24 | } 25 | 26 | let tokenB = sut.attach { value in 27 | receivedEvents.withValue { $0.append("b" + value) } 28 | } 29 | 30 | sut.emit("1") 31 | sut.emit("2") 32 | sut.emit("3") 33 | sut.emit("4") 34 | 35 | sut.emit("5", to: tokenA) 36 | sut.emit("6", to: tokenB) 37 | 38 | tokenA.cancel() 39 | 40 | sut.emit("7") 41 | sut.emit("8") 42 | 43 | XCTAssertEqual(sut.lastEvent, "8") 44 | 45 | XCTAssertEqual( 46 | receivedEvents.value, 47 | ["a0", "b0", "a1", "b1", "a2", "b2", "a3", "b3", "a4", "b4", "a5", "b6", "b7", "b8"] 48 | ) 49 | } 50 | 51 | func test_dontEmitLastEventWhenAttaching() { 52 | let sut = EventEmitter(initialEvent: "0", emitsLastEventWhenAttaching: false) 53 | XCTAssertFalse(sut.emitsLastEventWhenAttaching) 54 | 55 | let receivedEvent = LockIsolated<[String]>([]) 56 | let token = sut.attach { value in 57 | receivedEvent.withValue { $0.append(value) } 58 | } 59 | 60 | sut.emit("1") 61 | 62 | XCTAssertEqual(receivedEvent.value, ["1"]) 63 | 64 | token.cancel() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/AuthTests/Resources/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6Imd1aWxoZXJtZTJAZ3Jkcy5kZXYiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.4lMvmz2pJkWu1hMsBgXP98Fwz4rbvFYl4VA9joRv6kY", 3 | "token_type": "bearer", 4 | "expires_in": 3600, 5 | "expires_at": 345345345, 6 | "refresh_token": "GGduTeu95GraIXQ56jppkw", 7 | "user": { 8 | "id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", 9 | "aud": "authenticated", 10 | "role": "authenticated", 11 | "email": "guilherme@binaryscraping.co", 12 | "email_confirmed_at": "2022-03-30T10:33:41.018575157Z", 13 | "phone": "", 14 | "last_sign_in_at": "2022-03-30T10:33:41.021531328Z", 15 | "app_metadata": { 16 | "provider": "email", 17 | "providers": [ 18 | "email" 19 | ] 20 | }, 21 | "user_metadata": {}, 22 | "identities": [ 23 | { 24 | "id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", 25 | "user_id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", 26 | "identity_id": "859f402d-b3de-4105-a1b9-932836d9193b", 27 | "identity_data": { 28 | "sub": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8" 29 | }, 30 | "provider": "email", 31 | "last_sign_in_at": "2022-03-30T10:33:41.015557063Z", 32 | "created_at": "2022-03-30T10:33:41.015612Z", 33 | "updated_at": "2022-03-30T10:33:41.015616Z" 34 | } 35 | ], 36 | "created_at": "2022-03-30T10:33:41.005433Z", 37 | "updated_at": "2022-03-30T10:33:41.022688Z" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/RealtimeTests/RealtimeErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimeErrorTests.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 29/07/25. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import Realtime 11 | 12 | final class RealtimeErrorTests: XCTestCase { 13 | func testRealtimeErrorInitialization() { 14 | let errorMessage = "Connection failed" 15 | let error = RealtimeError(errorMessage) 16 | 17 | XCTAssertEqual(error.errorDescription, errorMessage) 18 | } 19 | 20 | func testRealtimeErrorLocalizedDescription() { 21 | let errorMessage = "Test error message" 22 | let error = RealtimeError(errorMessage) 23 | 24 | // LocalizedError protocol provides localizedDescription 25 | XCTAssertEqual(error.localizedDescription, errorMessage) 26 | } 27 | 28 | func testRealtimeErrorWithEmptyMessage() { 29 | let error = RealtimeError("") 30 | XCTAssertEqual(error.errorDescription, "") 31 | } 32 | 33 | func testRealtimeErrorAsError() { 34 | let errorMessage = "Network timeout" 35 | let realtimeError = RealtimeError(errorMessage) 36 | let error: Error = realtimeError 37 | 38 | // Test that it can be used as a general Error 39 | XCTAssertNotNil(error) 40 | XCTAssertEqual(error.localizedDescription, errorMessage) 41 | } 42 | 43 | func testRealtimeErrorEquality() { 44 | let error1 = RealtimeError("Same message") 45 | let error2 = RealtimeError("Same message") 46 | let error3 = RealtimeError("Different message") 47 | 48 | // Since RealtimeError doesn't implement Equatable, we test the description 49 | XCTAssertEqual(error1.errorDescription, error2.errorDescription) 50 | XCTAssertNotEqual(error1.errorDescription, error3.errorDescription) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Helpers/HTTP/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClient.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 30/04/24. 6 | // 7 | 8 | import Foundation 9 | 10 | #if canImport(FoundationNetworking) 11 | import FoundationNetworking 12 | #endif 13 | 14 | package protocol HTTPClientType: Sendable { 15 | func send(_ request: HTTPRequest) async throws -> HTTPResponse 16 | } 17 | 18 | package actor HTTPClient: HTTPClientType { 19 | let fetch: @Sendable (URLRequest) async throws -> (Data, URLResponse) 20 | let interceptors: [any HTTPClientInterceptor] 21 | 22 | package init( 23 | fetch: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse), 24 | interceptors: [any HTTPClientInterceptor] 25 | ) { 26 | self.fetch = fetch 27 | self.interceptors = interceptors 28 | } 29 | 30 | package func send(_ request: HTTPRequest) async throws -> HTTPResponse { 31 | var next: @Sendable (HTTPRequest) async throws -> HTTPResponse = { _request in 32 | let urlRequest = _request.urlRequest 33 | let (data, response) = try await self.fetch(urlRequest) 34 | guard let httpURLResponse = response as? HTTPURLResponse else { 35 | throw URLError(.badServerResponse) 36 | } 37 | return HTTPResponse(data: data, response: httpURLResponse) 38 | } 39 | 40 | for interceptor in interceptors.reversed() { 41 | let tmp = next 42 | next = { 43 | try await interceptor.intercept($0, next: tmp) 44 | } 45 | } 46 | 47 | return try await next(request) 48 | } 49 | } 50 | 51 | package protocol HTTPClientInterceptor: Sendable { 52 | func intercept( 53 | _ request: HTTPRequest, 54 | next: @Sendable (HTTPRequest) async throws -> HTTPResponse 55 | ) async throws -> HTTPResponse 56 | } 57 | -------------------------------------------------------------------------------- /scripts/check-for-breaking-api-changes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftOpenAPIGenerator open source project 5 | ## 6 | ## Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | set -euo pipefail 17 | 18 | log() { printf -- "** %s\n" "$*" >&2; } 19 | error() { printf -- "** ERROR: %s\n" "$*" >&2; } 20 | fatal() { error "$@"; exit 1; } 21 | 22 | CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 23 | REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" 24 | 25 | log "Checking required environment variables..." 26 | test -n "${BASELINE_REPO_URL:-}" || fatal "BASELINE_REPO_URL unset" 27 | test -n "${BASELINE_TREEISH:-}" || fatal "BASELINE_TREEISH unset" 28 | 29 | log "Fetching baseline: ${BASELINE_REPO_URL}#${BASELINE_TREEISH}..." 30 | git -C "${REPO_ROOT}" fetch "${BASELINE_REPO_URL}" "${BASELINE_TREEISH}" 31 | BASELINE_COMMIT=$(git -C "${REPO_ROOT}" rev-parse FETCH_HEAD) 32 | 33 | log "Checking for API changes since ${BASELINE_REPO_URL}#${BASELINE_TREEISH} (${BASELINE_COMMIT})..." 34 | swift package --package-path "${REPO_ROOT}" diagnose-api-breaking-changes \ 35 | "${BASELINE_COMMIT}" \ 36 | && RC=$? || RC=$? 37 | 38 | if [ "${RC}" -ne 0 ]; then 39 | fatal "❌ Breaking API changes detected." 40 | exit "${RC}" 41 | fi 42 | log "✅ No breaking API changes detected." 43 | -------------------------------------------------------------------------------- /Sources/Helpers/Logger/OSLogSupabaseLogger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(OSLog) 4 | import OSLog 5 | 6 | /// A SupabaseLogger implementation that logs to OSLog. 7 | /// 8 | /// This logger maps Supabase log levels to appropriate OSLog levels: 9 | /// - `.verbose` → `.info` 10 | /// - `.debug` → `.debug` 11 | /// - `.warning` → `.notice` 12 | /// - `.error` → `.error` 13 | /// 14 | /// ## Usage 15 | /// 16 | /// ```swift 17 | /// let supabaseLogger = OSLogSupabaseLogger() 18 | /// 19 | /// // Use with Supabase client 20 | /// let supabase = SupabaseClient( 21 | /// supabaseURL: url, 22 | /// supabaseKey: key, 23 | /// options: .init(global: .init(logger: supabaseLogger)) 24 | /// ) 25 | /// ``` 26 | @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) 27 | public struct OSLogSupabaseLogger: SupabaseLogger { 28 | private let logger: Logger 29 | 30 | /// Creates a new OSLog-based logger with a provided Logger instance. 31 | /// 32 | /// - Parameter logger: The OSLog Logger instance to use for logging. 33 | public init( 34 | _ logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "Supabase") 35 | ) { 36 | self.logger = logger 37 | } 38 | 39 | public func log(message: SupabaseLogMessage) { 40 | let logMessage = message.description 41 | 42 | switch message.level { 43 | case .verbose: 44 | logger.info("\(logMessage, privacy: .public)") 45 | case .debug: 46 | logger.debug("\(logMessage, privacy: .public)") 47 | case .warning: 48 | logger.notice("\(logMessage, privacy: .public)") 49 | case .error: 50 | logger.error("\(logMessage, privacy: .public)") 51 | } 52 | } 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /Examples/Examples/Realtime/RealtimeExamplesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimeExamplesView.swift 3 | // Examples 4 | // 5 | // Demonstrates Supabase Realtime features 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RealtimeExamplesView: View { 11 | var body: some View { 12 | List { 13 | Section { 14 | Text( 15 | "Subscribe to real-time changes in your database and communicate with presence and broadcast" 16 | ) 17 | .font(.caption) 18 | .foregroundColor(.secondary) 19 | } 20 | 21 | Section("Database Changes") { 22 | NavigationLink(destination: PostgresChangesView()) { 23 | ExampleRow( 24 | title: "Postgres Changes", 25 | description: "Listen to INSERT, UPDATE, DELETE events", 26 | icon: "antenna.radiowaves.left.and.right" 27 | ) 28 | } 29 | 30 | NavigationLink(destination: TodoRealtimeView()) { 31 | ExampleRow( 32 | title: "Live Todo List", 33 | description: "Real-time todo updates", 34 | icon: "checklist" 35 | ) 36 | } 37 | } 38 | 39 | Section("Broadcast") { 40 | NavigationLink(destination: BroadcastView()) { 41 | ExampleRow( 42 | title: "Broadcast Messages", 43 | description: "Send and receive broadcast events", 44 | icon: "megaphone" 45 | ) 46 | } 47 | } 48 | 49 | Section("Presence") { 50 | NavigationLink(destination: PresenceView()) { 51 | ExampleRow( 52 | title: "Presence Tracking", 53 | description: "Track online users in real-time", 54 | icon: "person.3" 55 | ) 56 | } 57 | } 58 | } 59 | .navigationTitle("Realtime") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/FunctionsTests/FunctionInvokeOptionsTests.swift: -------------------------------------------------------------------------------- 1 | import HTTPTypes 2 | import XCTest 3 | 4 | @testable import Functions 5 | 6 | final class FunctionInvokeOptionsTests: XCTestCase { 7 | func test_initWithStringBody() { 8 | let options = FunctionInvokeOptions(body: "string value") 9 | XCTAssertEqual(options.headers[.contentType], "text/plain") 10 | XCTAssertNotNil(options.body) 11 | } 12 | 13 | func test_initWithDataBody() { 14 | let options = FunctionInvokeOptions(body: "binary value".data(using: .utf8)!) 15 | XCTAssertEqual(options.headers[.contentType], "application/octet-stream") 16 | XCTAssertNotNil(options.body) 17 | } 18 | 19 | func test_initWithEncodableBody() { 20 | struct Body: Encodable { 21 | let value: String 22 | } 23 | let options = FunctionInvokeOptions(body: Body(value: "value")) 24 | XCTAssertEqual(options.headers[.contentType], "application/json") 25 | XCTAssertNotNil(options.body) 26 | } 27 | 28 | func test_initWithCustomContentType() { 29 | let boundary = "Boundary-\(UUID().uuidString)" 30 | let contentType = "multipart/form-data; boundary=\(boundary)" 31 | let options = FunctionInvokeOptions( 32 | headers: ["Content-Type": contentType], 33 | body: "binary value".data(using: .utf8)! 34 | ) 35 | XCTAssertEqual(options.headers[.contentType], contentType) 36 | XCTAssertNotNil(options.body) 37 | } 38 | 39 | func testMethod() { 40 | let testCases: [FunctionInvokeOptions.Method: HTTPTypes.HTTPRequest.Method] = [ 41 | .get: .get, 42 | .post: .post, 43 | .put: .put, 44 | .patch: .patch, 45 | .delete: .delete, 46 | ] 47 | 48 | for (method, expected) in testCases { 49 | XCTAssertEqual(FunctionInvokeOptions.httpMethod(method), expected) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/TestHelpers/HTTPClientMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClientMock.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 26/04/24. 6 | // 7 | 8 | import ConcurrencyExtras 9 | import Foundation 10 | import XCTestDynamicOverlay 11 | 12 | package actor HTTPClientMock: HTTPClientType { 13 | package struct MockNotFound: Error {} 14 | 15 | private var mocks = [@Sendable (HTTPRequest) async throws -> HTTPResponse?]() 16 | 17 | /// Requests received by this client in order. 18 | package var receivedRequests: [HTTPRequest] = [] 19 | 20 | /// Responses returned by this client in order. 21 | package var returnedResponses: [Result] = [] 22 | 23 | package init() {} 24 | 25 | @discardableResult 26 | package func when( 27 | _ request: @escaping @Sendable (HTTPRequest) -> Bool, 28 | return response: @escaping @Sendable (HTTPRequest) async throws -> HTTPResponse 29 | ) -> Self { 30 | mocks.append { r in 31 | if request(r) { 32 | return try await response(r) 33 | } 34 | return nil 35 | } 36 | return self 37 | } 38 | 39 | @discardableResult 40 | package func any( 41 | _ response: @escaping @Sendable (HTTPRequest) async throws -> HTTPResponse 42 | ) -> Self { 43 | when({ _ in true }, return: response) 44 | } 45 | 46 | package func send(_ request: HTTPRequest) async throws -> HTTPResponse { 47 | receivedRequests.append(request) 48 | 49 | for mock in mocks { 50 | do { 51 | if let response = try await mock(request) { 52 | returnedResponses.append(.success(response)) 53 | return response 54 | } 55 | } catch { 56 | returnedResponses.append(.failure(error)) 57 | throw error 58 | } 59 | } 60 | 61 | XCTFail("Mock not found for: \(request)") 62 | throw MockNotFound() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/PostgRESTTests/PostgrestResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import PostgREST 4 | 5 | #if canImport(FoundationNetworking) 6 | import FoundationNetworking 7 | #endif 8 | 9 | class PostgrestResponseTests: XCTestCase { 10 | func testInit() { 11 | // Prepare data and response 12 | let data = Data() 13 | let response = HTTPURLResponse( 14 | url: URL(string: "http://example.com")!, 15 | statusCode: 200, 16 | httpVersion: nil, 17 | headerFields: ["Content-Range": "bytes 0-100/200"] 18 | )! 19 | let value = "Test Value" 20 | 21 | // Create the PostgrestResponse instance 22 | let postgrestResponse = PostgrestResponse(data: data, response: response, value: value) 23 | 24 | // Assert the properties 25 | XCTAssertEqual(postgrestResponse.data, data) 26 | XCTAssertEqual(postgrestResponse.response, response) 27 | XCTAssertEqual(postgrestResponse.value, value) 28 | XCTAssertEqual(postgrestResponse.status, 200) 29 | XCTAssertEqual(postgrestResponse.count, 200) 30 | } 31 | 32 | func testInitWithNoCount() { 33 | // Prepare data and response 34 | let data = Data() 35 | let response = HTTPURLResponse( 36 | url: URL(string: "http://example.com")!, 37 | statusCode: 200, 38 | httpVersion: nil, 39 | headerFields: ["Content-Range": "*"] 40 | )! 41 | let value = "Test Value" 42 | 43 | // Create the PostgrestResponse instance 44 | let postgrestResponse = PostgrestResponse(data: data, response: response, value: value) 45 | 46 | // Assert the properties 47 | XCTAssertEqual(postgrestResponse.data, data) 48 | XCTAssertEqual(postgrestResponse.response, response) 49 | XCTAssertEqual(postgrestResponse.value, value) 50 | XCTAssertEqual(postgrestResponse.status, 200) 51 | XCTAssertNil(postgrestResponse.count) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Examples/Examples/Storage/BucketList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BucketList.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 21/03/24. 6 | // 7 | 8 | import Supabase 9 | import SwiftUI 10 | 11 | struct BucketList: View { 12 | @State var buckets = ActionState<[Bucket], Error>.idle 13 | 14 | var body: some View { 15 | Group { 16 | switch buckets { 17 | case .idle: 18 | Color.clear 19 | case .inFlight: 20 | ProgressView() 21 | case .result(.success(let buckets)): 22 | List { 23 | ForEach(buckets, id: \.self) { bucket in 24 | NavigationLink(bucket.name, value: bucket) 25 | } 26 | } 27 | .overlay { 28 | if buckets.isEmpty { 29 | Text("No buckets found.") 30 | } 31 | } 32 | case .result(.failure(let error)): 33 | VStack { 34 | ErrorText(error) 35 | Button("Retry") { 36 | Task { 37 | await load() 38 | } 39 | } 40 | } 41 | } 42 | } 43 | .task { 44 | await load() 45 | } 46 | .navigationTitle("All buckets") 47 | .toolbar { 48 | ToolbarItem(placement: .primaryAction) { 49 | Button("Add") { 50 | Task { 51 | do { 52 | try await supabase.storage.createBucket("bucket-\(UUID().uuidString)") 53 | await load() 54 | } catch {} 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | @MainActor 62 | private func load() async { 63 | do { 64 | self.buckets = .inFlight 65 | let buckets = try await supabase.storage.listBuckets() 66 | self.buckets = .result(.success(buckets)) 67 | } catch { 68 | buckets = .result(.failure(error)) 69 | } 70 | } 71 | } 72 | 73 | #Preview { 74 | BucketList() 75 | } 76 | -------------------------------------------------------------------------------- /Tests/AuthTests/PKCETests.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | import XCTest 3 | 4 | @testable import Auth 5 | 6 | final class PKCETests: XCTestCase { 7 | let sut = PKCE.live 8 | 9 | func testGenerateCodeVerifierLength() { 10 | // The code verifier should generate a string of appropriate length 11 | // Base64 encoding of 64 random bytes should result in ~86 characters 12 | let verifier = sut.generateCodeVerifier() 13 | XCTAssertGreaterThanOrEqual(verifier.count, 85) 14 | XCTAssertLessThanOrEqual(verifier.count, 87) 15 | } 16 | 17 | func testGenerateCodeVerifierUniqueness() { 18 | // Each generated code verifier should be unique 19 | let verifier1 = sut.generateCodeVerifier() 20 | let verifier2 = sut.generateCodeVerifier() 21 | XCTAssertNotEqual(verifier1, verifier2) 22 | } 23 | 24 | func testGenerateCodeChallenge() { 25 | // Test with a known input-output pair 26 | let testVerifier = "test_verifier" 27 | let challenge = sut.generateCodeChallenge(testVerifier) 28 | 29 | // Expected value from the current implementation 30 | let expectedChallenge = "0Ku4rR8EgR1w3HyHLBCxVLtPsAAks5HOlpmTEt0XhVA" 31 | XCTAssertEqual(challenge, expectedChallenge) 32 | } 33 | 34 | func testPKCEBase64Encoding() { 35 | // Create data that will produce Base64 with special characters 36 | let testData = Data([251, 255, 191]) // This will produce Base64 with padding and special chars 37 | let encoded = testData.pkceBase64EncodedString() 38 | 39 | XCTAssertFalse(encoded.contains("+"), "Should not contain '+'") 40 | XCTAssertFalse(encoded.contains("/"), "Should not contain '/'") 41 | XCTAssertFalse(encoded.contains("="), "Should not contain '='") 42 | XCTAssertTrue(encoded.contains("-"), "Should contain '-' as replacement for '+'") 43 | XCTAssertTrue(encoded.contains("_"), "Should contain '_' as replacement for '/'") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Examples/UserManagement/AuthView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthView.swift 3 | // UserManagement 4 | // 5 | // Created by Guilherme Souza on 17/11/23. 6 | // 7 | 8 | import Supabase 9 | import SwiftUI 10 | 11 | @MainActor 12 | struct AuthView: View { 13 | @State var email = "" 14 | @State var isLoading = false 15 | @State var result: Result? 16 | 17 | var body: some View { 18 | Form { 19 | Section { 20 | TextField("Email", text: $email) 21 | .textContentType(.emailAddress) 22 | .autocorrectionDisabled() 23 | #if os(iOS) 24 | .textInputAutocapitalization(.never) 25 | #endif 26 | } 27 | 28 | Section { 29 | Button("Sign in") { 30 | signInButtonTapped() 31 | } 32 | 33 | if isLoading { 34 | ProgressView() 35 | } 36 | } 37 | 38 | if let result { 39 | Section { 40 | switch result { 41 | case .success: Text("Check you inbox.") 42 | case .failure(let error): Text(error.localizedDescription).foregroundStyle(.red) 43 | } 44 | } 45 | } 46 | } 47 | .onMac { $0.padding() } 48 | .onOpenURL(perform: { url in 49 | Task { 50 | do { 51 | try await supabase.auth.session(from: url) 52 | } catch { 53 | result = .failure(error) 54 | } 55 | } 56 | }) 57 | } 58 | 59 | func signInButtonTapped() { 60 | Task { 61 | isLoading = true 62 | defer { isLoading = false } 63 | 64 | do { 65 | try await supabase.auth.signInWithOTP( 66 | email: email, 67 | redirectTo: URL(string: "io.supabase.user-management://login-callback") 68 | ) 69 | result = .success(()) 70 | } catch { 71 | result = .failure(error) 72 | } 73 | } 74 | } 75 | } 76 | 77 | #Preview { 78 | AuthView() 79 | } 80 | -------------------------------------------------------------------------------- /Tests/RealtimeTests/RealtimePostgresFilterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimePostgresFilterTests.swift 3 | // Supabase 4 | // 5 | // Created by Lucas Abijmil on 20/02/2025. 6 | // 7 | 8 | import XCTest 9 | @testable import Realtime 10 | 11 | final class RealtimePostgresFilterTests: XCTestCase { 12 | 13 | func testEq() { 14 | let value = "value" 15 | let column = "column" 16 | let filter: RealtimePostgresFilter = .eq(column, value: value) 17 | 18 | XCTAssertEqual(filter.value, "column=eq.value") 19 | } 20 | 21 | func testNeq() { 22 | let value = "value" 23 | let column = "column" 24 | let filter: RealtimePostgresFilter = .neq(column, value: value) 25 | 26 | XCTAssertEqual(filter.value, "column=neq.value") 27 | } 28 | 29 | func testGt() { 30 | let value = "value" 31 | let column = "column" 32 | let filter: RealtimePostgresFilter = .gt(column, value: value) 33 | 34 | XCTAssertEqual(filter.value, "column=gt.value") 35 | } 36 | 37 | func testGte() { 38 | let value = "value" 39 | let column = "column" 40 | let filter: RealtimePostgresFilter = .gte(column, value: value) 41 | 42 | XCTAssertEqual(filter.value, "column=gte.value") 43 | } 44 | 45 | func testLt() { 46 | let value = "value" 47 | let column = "column" 48 | let filter: RealtimePostgresFilter = .lt(column, value: value) 49 | 50 | XCTAssertEqual(filter.value, "column=lt.value") 51 | } 52 | 53 | func testLte() { 54 | let value = "value" 55 | let column = "column" 56 | let filter: RealtimePostgresFilter = .lte(column, value: value) 57 | 58 | XCTAssertEqual(filter.value, "column=lte.value") 59 | } 60 | 61 | func testIn() { 62 | let values = ["value1", "value2"] 63 | let column = "column" 64 | let filter: RealtimePostgresFilter = .in(column, values: values) 65 | 66 | XCTAssertEqual(filter.value, "column=in.(value1,value2)") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Examples/Examples/Storage/FileObjectDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileObjectDetailView.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 21/03/24. 6 | // 7 | 8 | import Supabase 9 | import SwiftUI 10 | 11 | struct FileObjectDetailView: View { 12 | let api: StorageFileApi 13 | let fileObject: FileObject 14 | 15 | @Environment(\.openURL) var openURL 16 | @State var lastActionResult: (action: String, result: Any)? 17 | 18 | var body: some View { 19 | List { 20 | Section { 21 | AnyJSONView(value: try! AnyJSON(fileObject)) 22 | } 23 | 24 | Section("Actions") { 25 | Button("createSignedURL") { 26 | Task { 27 | do { 28 | let url = try await api.createSignedURL(path: fileObject.name, expiresIn: 60) 29 | lastActionResult = ("createSignedURL", url) 30 | openURL(url) 31 | } catch {} 32 | } 33 | } 34 | 35 | Button("createSignedURL (download)") { 36 | Task { 37 | do { 38 | let url = try await api.createSignedURL( 39 | path: fileObject.name, 40 | expiresIn: 60, 41 | download: true 42 | ) 43 | lastActionResult = ("createSignedURL (download)", url) 44 | openURL(url) 45 | } catch {} 46 | } 47 | } 48 | 49 | Button("Get info") { 50 | Task { 51 | do { 52 | let info = try await api.info(path: fileObject.name) 53 | lastActionResult = ("info", info) 54 | } catch {} 55 | } 56 | } 57 | } 58 | 59 | if let lastActionResult { 60 | Section("Last action result") { 61 | Text(lastActionResult.action) 62 | Text(stringfy(lastActionResult.result)) 63 | } 64 | } 65 | } 66 | .navigationTitle(fileObject.name) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for supabase-swift 3 | title: "[Feature]: " 4 | labels: ["enhancement", "triage"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for suggesting a new feature! Please fill out the sections below. 12 | 13 | - type: checkboxes 14 | id: search 15 | attributes: 16 | label: Searched existing issues? 17 | description: Please search existing issues to avoid duplicates 18 | options: 19 | - label: I have searched the existing issues 20 | required: true 21 | 22 | - type: textarea 23 | id: problem 24 | attributes: 25 | label: Problem Description 26 | description: Is your feature request related to a problem? Please describe. 27 | placeholder: I'm always frustrated when... 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: solution 33 | attributes: 34 | label: Proposed Solution 35 | description: Describe the solution you'd like 36 | placeholder: I would like to see... 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: alternatives 42 | attributes: 43 | label: Alternative Solutions 44 | description: Describe any alternative solutions or features you've considered 45 | 46 | - type: dropdown 47 | id: priority 48 | attributes: 49 | label: Priority 50 | description: How important is this feature to you? 51 | options: 52 | - Low - Nice to have 53 | - Medium - Would significantly improve my workflow 54 | - High - Blocking my use case 55 | validations: 56 | required: true 57 | 58 | - type: textarea 59 | id: context 60 | attributes: 61 | label: Additional Context 62 | description: Add any other context, screenshots, or examples about the feature request -------------------------------------------------------------------------------- /Sources/Helpers/JWT.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWT.swift 3 | // Supabase 4 | // 5 | // Created by Guilherme Souza on 28/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | package struct DecodedJWT { 11 | package let header: [String: Any] 12 | package let payload: [String: Any] 13 | package let signature: Data 14 | package let raw: (header: String, payload: String) 15 | } 16 | 17 | package enum JWT { 18 | package static func decodePayload(_ jwt: String) -> [String: Any]? { 19 | let parts = jwt.split(separator: ".") 20 | guard parts.count == 3 else { 21 | return nil 22 | } 23 | 24 | let payload = String(parts[1]) 25 | guard let data = Base64URL.decode(payload) else { 26 | return nil 27 | } 28 | let json = try? JSONSerialization.jsonObject(with: data, options: []) 29 | guard let decodedPayload = json as? [String: Any] else { 30 | return nil 31 | } 32 | return decodedPayload 33 | } 34 | 35 | package static func decode(_ jwt: String) -> DecodedJWT? { 36 | let parts = jwt.split(separator: ".") 37 | guard parts.count == 3 else { 38 | return nil 39 | } 40 | 41 | let headerString = String(parts[0]) 42 | let payloadString = String(parts[1]) 43 | let signatureString = String(parts[2]) 44 | 45 | guard 46 | let headerData = Base64URL.decode(headerString), 47 | let payloadData = Base64URL.decode(payloadString), 48 | let signatureData = Base64URL.decode(signatureString), 49 | let headerJSON = try? JSONSerialization.jsonObject(with: headerData, options: []) as? [String: Any], 50 | let payloadJSON = try? JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any] 51 | else { 52 | return nil 53 | } 54 | 55 | return DecodedJWT( 56 | header: headerJSON, 57 | payload: payloadJSON, 58 | signature: signatureData, 59 | raw: (header: headerString, payload: payloadString) 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Helpers/HTTP/LoggerInterceptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggerInterceptor.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 30/04/24. 6 | // 7 | 8 | import Foundation 9 | 10 | package struct LoggerInterceptor: HTTPClientInterceptor { 11 | let logger: any SupabaseLogger 12 | 13 | package init(logger: any SupabaseLogger) { 14 | self.logger = logger 15 | } 16 | 17 | package func intercept( 18 | _ request: HTTPRequest, 19 | next: @Sendable (HTTPRequest) async throws -> HTTPResponse 20 | ) async throws -> HTTPResponse { 21 | let id = UUID().uuidString 22 | return try await SupabaseLoggerTaskLocal.$additionalContext.withValue(merging: ["requestID": .string(id)]) { 23 | let urlRequest = request.urlRequest 24 | 25 | logger.verbose( 26 | """ 27 | Request: \(urlRequest.httpMethod ?? "") \(urlRequest.url?.absoluteString.removingPercentEncoding ?? "") 28 | Body: \(stringfy(request.body)) 29 | """ 30 | ) 31 | 32 | do { 33 | let response = try await next(request) 34 | logger.verbose( 35 | """ 36 | Response: Status code: \(response.statusCode) Content-Length: \( 37 | response.underlyingResponse.expectedContentLength 38 | ) 39 | Body: \(stringfy(response.data)) 40 | """ 41 | ) 42 | return response 43 | } catch { 44 | logger.error("Response: Failure \(error)") 45 | throw error 46 | } 47 | } 48 | } 49 | } 50 | 51 | func stringfy(_ data: Data?) -> String { 52 | guard let data else { 53 | return "" 54 | } 55 | 56 | do { 57 | let object = try JSONSerialization.jsonObject(with: data, options: []) 58 | let prettyData = try JSONSerialization.data( 59 | withJSONObject: object, 60 | options: [.prettyPrinted, .sortedKeys] 61 | ) 62 | return String(data: prettyData, encoding: .utf8) ?? "" 63 | } catch { 64 | return String(data: data, encoding: .utf8) ?? "" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/FunctionsTests/RequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestTests.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 23/04/24. 6 | // 7 | 8 | @testable import Functions 9 | import SnapshotTesting 10 | import XCTest 11 | 12 | final class RequestTests: XCTestCase { 13 | let url = URL(string: "http://localhost:5432/functions/v1")! 14 | let apiKey = "supabase.anon.key" 15 | 16 | func testInvokeWithDefaultOptions() async { 17 | await snapshot { 18 | try await $0.invoke("hello-world") 19 | } 20 | } 21 | 22 | func testInvokeWithCustomMethod() async { 23 | await snapshot { 24 | try await $0.invoke("hello-world", options: .init(method: .patch)) 25 | } 26 | } 27 | 28 | func testInvokeWithCustomRegion() async { 29 | await snapshot { 30 | try await $0.invoke("hello-world", options: .init(region: .apNortheast1)) 31 | } 32 | } 33 | 34 | func testInvokeWithCustomHeader() async { 35 | await snapshot { 36 | try await $0.invoke("hello-world", options: .init(headers: ["x-custom-key": "custom value"])) 37 | } 38 | } 39 | 40 | func testInvokeWithBody() async { 41 | await snapshot { 42 | try await $0.invoke("hello-world", options: .init(body: ["name": "Supabase"])) 43 | } 44 | } 45 | 46 | func snapshot( 47 | record: Bool = false, 48 | _ test: (FunctionsClient) async throws -> Void, 49 | file: StaticString = #file, 50 | testName: String = #function, 51 | line: UInt = #line 52 | ) async { 53 | let sut = FunctionsClient( 54 | url: url, 55 | headers: ["apikey": apiKey, "x-client-info": "functions-swift/x.y.z"] 56 | ) { request in 57 | await MainActor.run { 58 | #if os(Android) 59 | // missing snapshots for Android 60 | return 61 | #endif 62 | assertSnapshot(of: request, as: .curl, record: record, file: file, testName: testName, line: line) 63 | } 64 | throw NSError(domain: "Error", code: 0, userInfo: nil) 65 | } 66 | 67 | try? await test(sut) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Helpers/Version.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTestDynamicOverlay 3 | 4 | private let _version = "2.38.1" // {x-release-please-version} 5 | 6 | #if DEBUG 7 | package let version = isTesting ? "0.0.0" : _version 8 | #else 9 | package let version = _version 10 | #endif 11 | 12 | private let _platform: String? = { 13 | #if os(macOS) 14 | return "macOS" 15 | #elseif os(visionOS) 16 | return "visionOS" 17 | #elseif os(iOS) 18 | #if targetEnvironment(macCatalyst) 19 | return "macCatalyst" 20 | #else 21 | if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac { 22 | return "iOSAppOnMac" 23 | } 24 | return "iOS" 25 | #endif 26 | #elseif os(watchOS) 27 | return "watchOS" 28 | #elseif os(tvOS) 29 | return "tvOS" 30 | #elseif os(Android) 31 | return "Android" 32 | #elseif os(Linux) 33 | return "Linux" 34 | #elseif os(Windows) 35 | return "Windows" 36 | #else 37 | return nil 38 | #endif 39 | }() 40 | 41 | private let _platformVersion: String? = { 42 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Windows) 43 | let majorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion 44 | let minorVersion = ProcessInfo.processInfo.operatingSystemVersion.minorVersion 45 | let patchVersion = ProcessInfo.processInfo.operatingSystemVersion.patchVersion 46 | return "\(majorVersion).\(minorVersion).\(patchVersion)" 47 | #elseif os(Linux) || os(Android) 48 | if let version = try? String(contentsOfFile: "/proc/version") { 49 | return version.trimmingCharacters(in: .whitespacesAndNewlines) 50 | } else { 51 | return nil 52 | } 53 | #else 54 | nil 55 | #endif 56 | }() 57 | 58 | #if DEBUG 59 | package let platform = isTesting ? "macOS" : _platform 60 | #else 61 | package let platform = _platform 62 | #endif 63 | 64 | #if DEBUG 65 | package let platformVersion = isTesting ? "0.0.0" : _platformVersion 66 | #else 67 | package let platformVersion = _platformVersion 68 | #endif 69 | -------------------------------------------------------------------------------- /Sources/Helpers/HTTP/HTTPFields.swift: -------------------------------------------------------------------------------- 1 | import HTTPTypes 2 | 3 | extension HTTPFields { 4 | package init(_ dictionary: [String: String]) { 5 | self.init(dictionary.map { .init(name: .init($0.key)!, value: $0.value) }) 6 | } 7 | 8 | package var dictionary: [String: String] { 9 | let keyValues = self.map { 10 | ($0.name.rawName, $0.value) 11 | } 12 | 13 | return .init(keyValues, uniquingKeysWith: { $1 }) 14 | } 15 | 16 | package mutating func merge(with other: Self) { 17 | for field in other { 18 | self[field.name] = field.value 19 | } 20 | } 21 | 22 | package func merging(with other: Self) -> Self { 23 | var copy = self 24 | 25 | for field in other { 26 | copy[field.name] = field.value 27 | } 28 | 29 | return copy 30 | } 31 | 32 | /// Append or update a value in header. 33 | /// 34 | /// Example: 35 | /// ```swift 36 | /// var headers: HTTPFields = [ 37 | /// "Prefer": "count=exact,return=representation" 38 | /// ] 39 | /// 40 | /// headers.appendOrUpdate(.prefer, value: "return=minimal") 41 | /// #expect(headers == ["Prefer": "count=exact,return=minimal"] 42 | /// ``` 43 | package mutating func appendOrUpdate( 44 | _ name: HTTPField.Name, 45 | value: String, 46 | separator: String = "," 47 | ) { 48 | if let currentValue = self[name] { 49 | var components = currentValue.components(separatedBy: separator) 50 | 51 | if let key = value.split(separator: "=").first, 52 | let index = components.firstIndex(where: { $0.hasPrefix("\(key)=") }) 53 | { 54 | components[index] = value 55 | } else { 56 | components.append(value) 57 | } 58 | 59 | self[name] = components.joined(separator: separator) 60 | } else { 61 | self[name] = value 62 | } 63 | } 64 | } 65 | 66 | extension HTTPField.Name { 67 | package static let xClientInfo = HTTPField.Name("X-Client-Info")! 68 | package static let xRegion = HTTPField.Name("x-region")! 69 | package static let xRelayError = HTTPField.Name("x-relay-error")! 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Storage/TransformOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Transform the asset before serving it to the client. 4 | public struct TransformOptions: Encodable, Sendable { 5 | /// The width of the image in pixels. 6 | public var width: Int? 7 | /// The height of the image in pixels. 8 | public var height: Int? 9 | /// The resize mode can be cover, contain or fill. Defaults to cover. 10 | /// Cover resizes the image to maintain it's aspect ratio while filling the entire width and height. 11 | /// Contain resizes the image to maintain it's aspect ratio while fitting the entire image within the width and height. 12 | /// Fill resizes the image to fill the entire width and height. If the object's aspect ratio does not match the width and height, the image will be stretched to fit. 13 | public var resize: String? 14 | /// Set the quality of the returned image. A number from 20 to 100, with 100 being the highest quality. Defaults to 80. 15 | public var quality: Int? 16 | /// Specify the format of the image requested. 17 | public var format: String? 18 | 19 | public init( 20 | width: Int? = nil, 21 | height: Int? = nil, 22 | resize: String? = nil, 23 | quality: Int? = 80, 24 | format: String? = nil 25 | ) { 26 | self.width = width 27 | self.height = height 28 | self.resize = resize 29 | self.quality = quality 30 | self.format = format 31 | } 32 | 33 | var queryItems: [URLQueryItem] { 34 | var items = [URLQueryItem]() 35 | 36 | if let width { 37 | items.append(URLQueryItem(name: "width", value: String(width))) 38 | } 39 | 40 | if let height { 41 | items.append(URLQueryItem(name: "height", value: String(height))) 42 | } 43 | 44 | if let resize { 45 | items.append(URLQueryItem(name: "resize", value: resize)) 46 | } 47 | 48 | if let quality { 49 | items.append(URLQueryItem(name: "quality", value: String(quality))) 50 | } 51 | 52 | if let format { 53 | items.append(URLQueryItem(name: "format", value: format)) 54 | } 55 | 56 | return items 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/IntegrationTests/StorageClientIntegrationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageClientIntegrationTests.swift 3 | // 4 | // 5 | // Created by Guilherme Souza on 07/05/24. 6 | // 7 | 8 | import InlineSnapshotTesting 9 | import Storage 10 | import XCTest 11 | 12 | final class StorageClientIntegrationTests: XCTestCase { 13 | let storage = SupabaseStorageClient( 14 | configuration: StorageClientConfiguration( 15 | url: URL(string: "\(DotEnv.SUPABASE_URL)/storage/v1")!, 16 | headers: [ 17 | "Authorization": "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)", 18 | ], 19 | logger: nil 20 | ) 21 | ) 22 | 23 | func testBucket_CRUD() async throws { 24 | let bucketName = "test-bucket" 25 | 26 | var buckets = try await storage.listBuckets() 27 | XCTAssertFalse(buckets.contains(where: { $0.name == bucketName })) 28 | 29 | try await storage.createBucket(bucketName, options: .init(public: true)) 30 | 31 | var bucket = try await storage.getBucket(bucketName) 32 | XCTAssertEqual(bucket.name, bucketName) 33 | XCTAssertEqual(bucket.id, bucketName) 34 | XCTAssertEqual(bucket.isPublic, true) 35 | 36 | buckets = try await storage.listBuckets() 37 | XCTAssertTrue(buckets.contains { $0.id == bucket.id }) 38 | 39 | try await storage.updateBucket(bucketName, options: BucketOptions(allowedMimeTypes: ["image/jpeg"])) 40 | 41 | bucket = try await storage.getBucket(bucketName) 42 | XCTAssertEqual(bucket.allowedMimeTypes, ["image/jpeg"]) 43 | 44 | try await storage.deleteBucket(bucketName) 45 | 46 | buckets = try await storage.listBuckets() 47 | XCTAssertFalse(buckets.contains { $0.id == bucket.id }) 48 | } 49 | 50 | func testGetBucketWithWrongId() async { 51 | do { 52 | _ = try await storage.getBucket("not-exist-id") 53 | XCTFail("Unexpected success") 54 | } catch { 55 | assertInlineSnapshot(of: error, as: .dump) { 56 | """ 57 | ▿ StorageError 58 | ▿ error: Optional 59 | - some: "Bucket not found" 60 | - message: "Bucket not found" 61 | ▿ statusCode: Optional 62 | - some: "404" 63 | 64 | """ 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Examples/Examples/Auth/SignInWithFacebook.swift: -------------------------------------------------------------------------------- 1 | import FacebookLogin 2 | import OSLog 3 | import Supabase 4 | import SwiftUI 5 | 6 | struct SignInWithFacebook: View { 7 | @State private var actionState = ActionState.idle 8 | 9 | static let logger = Logger(subsystem: "com.supabase.examples", category: "SignInWithFacebook") 10 | 11 | let loginManager = LoginManager() 12 | 13 | var body: some View { 14 | VStack { 15 | Button("Sign in with Facebook") { 16 | actionState = .inFlight 17 | 18 | loginManager.logIn( 19 | configuration: LoginConfiguration( 20 | permissions: ["public_profile", "email"], 21 | tracking: .limited 22 | ) 23 | ) { result in 24 | switch result { 25 | case .failed(let error): 26 | actionState = .result(.failure(error)) 27 | Self.logger.error("Facebook login failed: \(error.localizedDescription)") 28 | case .cancelled: 29 | actionState = .idle 30 | Self.logger.info("Facebook login cancelled") 31 | case .success(_, _, let token): 32 | Self.logger.info("Facebook login succeeded.") 33 | 34 | guard let idToken = token?.tokenString else { 35 | actionState = .idle 36 | Self.logger.error("Facebook login token is nil") 37 | return 38 | } 39 | 40 | Task { 41 | do { 42 | try await supabase.auth.signInWithIdToken( 43 | credentials: OpenIDConnectCredentials( 44 | provider: .facebook, 45 | idToken: idToken 46 | ) 47 | ) 48 | actionState = .result(.success(())) 49 | Self.logger.info("Successfully signed in with Facebook") 50 | } catch { 51 | actionState = .result(.failure(error)) 52 | Self.logger.error("Failed to sign in with Facebook: \(error.localizedDescription)") 53 | 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | .gitHubSourceLink() 61 | } 62 | } 63 | 64 | #Preview { 65 | SignInWithFacebook() 66 | } 67 | -------------------------------------------------------------------------------- /Examples/Examples/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // Examples 4 | // 5 | // Created by Guilherme Souza on 23/12/22. 6 | // 7 | 8 | import Supabase 9 | import SwiftUI 10 | 11 | struct HomeView: View { 12 | @Environment(AuthController.self) var auth 13 | 14 | var body: some View { 15 | @Bindable var auth = auth 16 | 17 | TabView { 18 | // Database Tab 19 | NavigationStack { 20 | DatabaseExamplesView() 21 | } 22 | .tabItem { 23 | Label("Database", systemImage: "cylinder.split.1x2") 24 | } 25 | 26 | // Realtime Tab 27 | NavigationStack { 28 | RealtimeExamplesView() 29 | } 30 | .tabItem { 31 | Label("Realtime", systemImage: "bolt") 32 | } 33 | 34 | // Storage Tab 35 | NavigationStack { 36 | StorageExamplesView() 37 | .navigationDestination(for: Bucket.self, destination: BucketDetailView.init) 38 | } 39 | .tabItem { 40 | Label("Storage", systemImage: "externaldrive") 41 | } 42 | 43 | // Functions Tab 44 | NavigationStack { 45 | FunctionsExamplesView() 46 | } 47 | .tabItem { 48 | Label("Functions", systemImage: "function") 49 | } 50 | 51 | // Profile Tab 52 | ProfileView() 53 | .tabItem { 54 | Label("Profile", systemImage: "person.circle") 55 | } 56 | } 57 | .sheet(isPresented: $auth.isPasswordRecoveryFlow) { 58 | UpdatePasswordView() 59 | } 60 | } 61 | 62 | struct UpdatePasswordView: View { 63 | @Environment(\.dismiss) var dismiss 64 | 65 | @State var password: String = "" 66 | 67 | var body: some View { 68 | Form { 69 | SecureField("Password", text: $password) 70 | .textContentType(.newPassword) 71 | 72 | Button("Update password") { 73 | Task { 74 | do { 75 | try await supabase.auth.update(user: UserAttributes(password: password)) 76 | dismiss() 77 | } catch {} 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | struct HomeView_Previews: PreviewProvider { 86 | static var previews: some View { 87 | HomeView() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/StorageTests/TransformOptionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import Storage 4 | 5 | final class TransformOptionsTests: XCTestCase { 6 | func testDefaultInitialization() { 7 | let options = TransformOptions() 8 | 9 | XCTAssertNil(options.width) 10 | XCTAssertNil(options.height) 11 | XCTAssertNil(options.resize) 12 | XCTAssertEqual(options.quality, 80) // Default value 13 | XCTAssertNil(options.format) 14 | } 15 | 16 | func testCustomInitialization() { 17 | let options = TransformOptions( 18 | width: 100, 19 | height: 200, 20 | resize: "cover", 21 | quality: 90, 22 | format: "webp" 23 | ) 24 | 25 | XCTAssertEqual(options.width, 100) 26 | XCTAssertEqual(options.height, 200) 27 | XCTAssertEqual(options.resize, "cover") 28 | XCTAssertEqual(options.quality, 90) 29 | XCTAssertEqual(options.format, "webp") 30 | } 31 | 32 | func testQueryItemsGeneration() { 33 | let options = TransformOptions( 34 | width: 100, 35 | height: 200, 36 | resize: "cover", 37 | quality: 90, 38 | format: "webp" 39 | ) 40 | 41 | let queryItems = options.queryItems 42 | 43 | XCTAssertEqual(queryItems.count, 5) 44 | 45 | XCTAssertEqual(queryItems[0].name, "width") 46 | XCTAssertEqual(queryItems[0].value, "100") 47 | 48 | XCTAssertEqual(queryItems[1].name, "height") 49 | XCTAssertEqual(queryItems[1].value, "200") 50 | 51 | XCTAssertEqual(queryItems[2].name, "resize") 52 | XCTAssertEqual(queryItems[2].value, "cover") 53 | 54 | XCTAssertEqual(queryItems[3].name, "quality") 55 | XCTAssertEqual(queryItems[3].value, "90") 56 | 57 | XCTAssertEqual(queryItems[4].name, "format") 58 | XCTAssertEqual(queryItems[4].value, "webp") 59 | } 60 | 61 | func testPartialQueryItemsGeneration() { 62 | let options = TransformOptions(width: 100, quality: 75) 63 | 64 | let queryItems = options.queryItems 65 | 66 | XCTAssertEqual(queryItems.count, 2) 67 | 68 | XCTAssertEqual(queryItems[0].name, "width") 69 | XCTAssertEqual(queryItems[0].value, "100") 70 | 71 | XCTAssertEqual(queryItems[1].name, "quality") 72 | XCTAssertEqual(queryItems[1].value, "75") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report to help us improve 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | 13 | - type: input 14 | id: version 15 | attributes: 16 | label: Version 17 | description: What version of supabase-swift are you running? 18 | placeholder: ex. 2.31.2 19 | validations: 20 | required: true 21 | 22 | - type: dropdown 23 | id: platform 24 | attributes: 25 | label: Platform 26 | description: What platform are you using? 27 | options: 28 | - iOS 29 | - macOS 30 | - tvOS 31 | - watchOS 32 | - visionOS 33 | - Linux 34 | - Other 35 | validations: 36 | required: true 37 | 38 | - type: input 39 | id: swift-version 40 | attributes: 41 | label: Swift Version 42 | description: What version of Swift are you using? 43 | placeholder: ex. 5.10 44 | validations: 45 | required: true 46 | 47 | - type: textarea 48 | id: what-happened 49 | attributes: 50 | label: What happened? 51 | description: Also tell us, what did you expect to happen? 52 | placeholder: Tell us what you see! 53 | validations: 54 | required: true 55 | 56 | - type: textarea 57 | id: reproduce 58 | attributes: 59 | label: Steps to Reproduce 60 | description: Please provide clear steps to reproduce the issue 61 | placeholder: | 62 | 1. Import Supabase 63 | 2. Create client with '...' 64 | 3. Call method '...' 65 | 4. See error 66 | validations: 67 | required: true 68 | 69 | - type: textarea 70 | id: code-sample 71 | attributes: 72 | label: Code Sample 73 | description: Please provide a minimal code sample that reproduces the issue 74 | render: swift 75 | 76 | - type: textarea 77 | id: logs 78 | attributes: 79 | label: Relevant log output 80 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 81 | render: shell --------------------------------------------------------------------------------