Merge branch 'supabase:master' into master

This commit is contained in:
Sara Tavares authored and GitHub committed 2022-10-19 14:39:18 +01:00
commit f703b1f762
75 files changed
+2622 -1133

No files matched your search

-27
View File
@@ -1,27 +0,0 @@
name: Deploy studio app to production on vercel
on:
push:
branches:
- studio
paths:
- 'studio/**'
jobs:
deploy:
runs-on: ubuntu-18.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
# remove ./studio/pages/api folder before deploying to vercel
- uses: JesseTG/rm@v1.0.2
with:
path: ./studio/pages/api
- uses: amondnet/vercel-action@v19
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
github-comment: false
vercel-args: '--prod -A .vercel/production.json'
vercel-org-id: team_E6KJ1W561hMTjon1QSwOh0WO #Required
vercel-project-id: Qmd7Mw8yFW8SbtvTWFV9uyRzw9P1rJLmgtHjSezywhtYec #Required
working-directory: ./studio #Your Working Directory, Optional
vercel-project-name: app
@@ -1,27 +0,0 @@
name: Deploy studio app to staging on vercel
on:
push:
branches:
- master
paths:
- 'studio/**'
jobs:
deploy:
runs-on: ubuntu-18.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
# remove ./studio/pages/api folder before deploying to vercel
- uses: JesseTG/rm@v1.0.2
with:
path: ./studio/pages/api
- uses: amondnet/vercel-action@v19
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
github-comment: false
vercel-args: '--prod -A .vercel/staging.json'
vercel-org-id: team_E6KJ1W561hMTjon1QSwOh0WO #Required
vercel-project-id: QmcmhbiAtCMFTAHCuGgQscNbke4TzgWULECctNcKmxWCoT #Required
working-directory: ./studio #Your Working Directory, Optional
vercel-project-name: staging-app
+12 -13
View File
@@ -9,7 +9,6 @@ on:
- 'studio/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
@@ -18,15 +17,15 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm i
working-directory: ./studio
- name: Run build
run: npm run build
working-directory: ./studio
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm ci
working-directory: ./
- name: Run build
run: npx turbo run build --filter=studio
working-directory: ./
+3
View File
@@ -16,6 +16,9 @@ The CLI is still under development, but it contains all the functionality for wo
- CI/CD for releasing to production: [`supabase db push`](https://supabase.com/docs/reference/cli/usage#supabase-db-push)
- Manage your Supabase projects: [`supabase projects`](https://supabase.com/docs/reference/cli/usage#supabase-projects)
- Generate types directly from your database schema: [`supabase gen types`](https://supabase.com/docs/reference/cli/usage#supabase-gen)
- A [community-supported GitHub Action](https://github.com/lyqht/generate-supabase-db-types-github-action) to generate TypeScript types
- Shell autocomplete: [`supabase completion`](https://supabase.com/docs/reference/cli/usage#supabase-completion)
- A [community-supported Fig autocomplete spec](https://fig.io/manual/supabase) for macOS terminal
## Additional Links
+143 -26
View File
@@ -2,38 +2,155 @@
id: architecture
title: Architecture
description: 'Supabase design and architecture'
# hide_table_of_contents: true
---
Supabase is open source. Wherever possible, we use and support existing tools rather than developing from scratch.
We choose open source tools which are scalable and we make them simple to use.
Supabase is open source. We choose open source tools which are scalable and make them simple to use.
Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way:
our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch.
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase,
while maintaining the scalability required to go beyond it.
## Architecture
Each Supabase project consists of several tools:
![Supabase Architecture](/img/supabase-architecture.png)
Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way.
### PostgreSQL (Database)
Our technological choices are quite different from Firebase. Everything we use is open source. Wherever possible, we use and support existing tools rather than developing from scratch.
PostgreSQL is the core of Supabase. We do not abstract the PostgreSQL database — you can access it and use it with full privileges. We simply provide tools which makes PostgreSQL as easy to use as Firebase.
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the scalability and functionality required to compete with Firebase.
- Official Docs: [postgresql.org/docs](https://www.postgresql.org/docs/current/index.html)
- Source code: [github.com/postgres/postgres](https://github.com/postgres/postgres) (mirror)
- License: [PostgreSQL Licence](https://www.postgresql.org/about/licence/)
- Language: C
## Feature Status
### Studio (Dashboard)
| Product | Feature | Stage | Docs |
| -------------------------- | ---------------------- | ----- | ------------------------------------------------ |
| Database | Postgres | GA | [Link](/docs/guides/database) |
| Database | Webhooks | Alpha | |
| Database | Point in time Recovery | Alpha | |
| Realtime | Postgres CDC | Beta | [Link](/docs/guides/realtime/postgres-cdc) |
| Realtime | Broadcast | Alpha | [Link](/docs/guides/realtime#broadcast) |
| Realtime | Presence | Alpha | [Link](/docs/guides/realtime#presence) |
| Storage | | Beta | [Link](/docs/guides/storage) |
| Storage | CDN | Beta | [Link](/docs/guides/storage-cdn) |
| Edge Functions | | Beta | [Link](/docs/guides/functions) |
| Auth | OAuth Providers | Beta | [Link](/docs/guides/auth/auth-apple) |
| Auth | Passwordless | Beta | [Link](/guides/auth/auth-magic-link) |
| Auth | Next.js Auth Helpers | Alpha | [Link](/docs/guides/auth/auth-helpers/nextjs) |
| Auth | SvelteKit Auth Helpers | Alpha | [Link](/docs/guides/auth/auth-helpers/sveltekit) |
| Public API | | Beta | [Link](/docs/reference/api) |
| CLI | | Beta | [Link](/docs/guides/cli) |
| Client Library: JavaScript | | GA | [Link](/docs/reference/javascript) |
| Client Library: Dart | | Beta | [Link](/docs/reference/dart) |
An open source Dashboard for managing your database and services.
- Official Docs: [Supabase docs](/docs)
- Source code: [github.com/supabase/supabase](https://github.com/supabase/supabase/tree/master/studio)
- License: [Apache 2](https://github.com/supabase/supabase/blob/master/LICENSE)
- Language: TypeScript
### GoTrue (Auth)
A JWT-based API for managing users and issuing access tokens. This integrates with PostgreSQL's Row Level Security and the API servers.
- Official Docs: [Supabase Auth reference docs](/docs/reference/auth)
- Source code: [github.com/supabase/gotrue](https://github.com/supabase/gotrue)
- License: [MIT](https://github.com/supabase/gotrue/blob/master/LICENSE)
- Language: Go
### PostgREST (API)
A standalone web server that turns your PostgreSQL database directly into a RESTful API.
We use this with our [pg_graphql](https://github.com/supabase/pg_graphql) extension to provide a GraphQL API.
- Official Docs: [postgrest.org](https://postgrest.org/)
- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest)
- License: [MIT](https://github.com/PostgREST/postgrest/blob/main/LICENSE)
- Language: Haskel
### Realtime (API & multiplayer)
A scalable websocket engine for managing user Presence, broadcasting messages, and streaming database changes.
- Official Docs: [Supabase Realtime docs](/docs/guides/realtime)
- Source code: [github.com/supabase/realtime](https://github.com/supabase/realtime)
- License: [Apache 2](https://github.com/supabase/realtime/blob/master/LICENSE)
- Language: Elixir
### Storage API (large file storage)
An S3-compatible object storage service that stores metadata in Postgres.
- Official Docs: [Supabase Storage reference docs](/docs/reference/storage)
- Source code: [github.com/supabase/storage-api](https://github.com/supabase/storage-api)
- License: [Apache 2.0](https://github.com/supabase/storage-api/blob/master/LICENSE)
- Language: NodeJS / TypeScript
### Deno (Edge Functions)
A modern runtime for JavaScript and TypeScript.
- Official Docs: [deno.land](https://deno.land/)
- Source code: [github.com/denoland/deno](https://github.com/denoland/deno)
- License: [MIT](https://github.com/denoland/deno/blob/main/LICENSE.md)
- Language: TypeScript / Rust
### postgres-meta (Database management)
A RESTful API for managing your Postgres. Fetch tables, add roles, and run queries.
- Official Docs: [supabase.github.io/postgres-meta](https://supabase.github.io/postgres-meta/)
- Source code: [github.com/supabase/postgres-meta](https://github.com/supabase/postgres-meta)
- License: [Apache 2.0](https://github.com/supabase/postgres-meta/blob/master/LICENSE)
- Language: NodeJS / TypeScript
### PgBouncer
A lightweight connection pooler for PostgreSQL. This is useful for connecting to Postgres when using Serverless functions.
- Official Docs: [pgbouncer.org](http://www.pgbouncer.org/)
- Source code: [pgbouncer/pgbouncer](https://github.com/pgbouncer/pgbouncer)
- License: [ISC](https://github.com/pgbouncer/pgbouncer/blob/master/COPYRIGHT)
- Language: C
### Kong (API Gateway)
A cloud-native API gateway, built on top of Nginx.
- Official Docs: [docs.konghq.com](https://docs.konghq.com/)
- Source code: [github.com/kong/kong](https://github.com/kong/kong)
- License: [Apache 2.0](https://github.com/Kong/kong/blob/master/LICENSE)
- Language: Lua
## Product Principles
It is our goal to provide an architecture that any large-scale company would design for themselves,
and then provide tooling around that architecture that is easy-to-use for indie-developers and small teams.
We use a series of principles to ensure that scalability and usability are never mutually exclusive:
### Everything works in isolation
Each system must work as a standalone tool with as few moving parts as possible.
The litmus test for this is: "Can a user run this product with nothing but a Postgres database?"
### Everything is integrated
Supabase is composable. Even though every product works in isolation, each product on the platform needs to 10x the other products.
For integration, each tool should expose an API and Webhooks.
### Everything is extensible
We're deliberate about adding a new tool, and prefer instead to extend an existing one.
This is the opposite of many cloud providers whose product offering expands into niche use-cases. We provide _primitives_ for developers, which allow them to achieve any goal.
Less, but better.
### Everything is portable
To avoid lock-in, we make it easy to migrate in and out. Our cloud offering is compatible with our self-hosted product.
We use existing standards to increase portability (like pg_dump an CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
This forces us compete on experience. We aim to be the best Postgres hosting service.
### Play the long game
We sacrifice short-term wins for long-term gains. For example, it is tempting to run a fork of Postgres with additional functionality which only our customers need.
Instead, we prefer to support efforts to upstream missing functionality so that the entire community benefits. This has the additional benefit of ensuring portability and longevity.
### Build for developers
"Developers" are a specific profile of user: they are _builders_.
When assessing impact as a function of effort, developers have a large efficiency due to the type of products and systems they can build.
As the profile of a developer changes over time, Supabase will continue to evolve the product to fit this evolving profile.
### Support existing tools
Supabase supports existing tools and communities wherever possible. Supabase is more like a "community of communities" - each tool typically has its own community
which we work with.
Open source is something we approach [collaboratively](https://supabase.com/blog/supabase-series-b#giving-back): we employ maintainers, sponsor projects, invest in businesses, and develop our own open source tools.
@@ -65,6 +65,10 @@ This will allow your users to use your custom Azure login page when logging in.
## Add login code to your client app
:::tip
Supabase Auth requires that Azure returns a valid email address. Therefore you must request the `email` scope in the `signIn` method above.
:::
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `azure` as the `provider`:
```js
@@ -31,7 +31,8 @@ This library supports the following tooling versions:
- Node.js: `^10.13.0 || >=12.0.0`
- Next.js: `>=10`
Optionally, install **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
```sh
npm install @supabase/auth-helpers-react
```
@@ -48,7 +49,7 @@ This library supports the following tooling versions:
- Node.js: `^10.13.0 || >=12.0.0`
- Next.js: `>=10`
Optionally, install **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
```sh
yarn add @supabase/auth-helpers-react
@@ -61,23 +62,30 @@ yarn add @supabase/auth-helpers-react
Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env.local` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/nextjs/.env.local.example).
```bash
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
```bash title=".env.local"
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
## Basic Setup
<Tabs
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
```jsx title="pages/_app.js"
import React from 'react'
import { useRouter } from 'next/router'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
function MyApp({ Component, pageProps }: AppProps) {
function MyApp({ Component, pageProps }) {
const router = useRouter()
// Create a new supabase browser client on every first render.
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
return (
@@ -85,58 +93,105 @@ function MyApp({ Component, pageProps }: AppProps) {
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<button
onClick={async () => {
await supabaseClient.auth.signOut()
router.push('/')
}}
>
Logout
</button>
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
</TabItem>
<TabItem value="ts">
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
```jsx title="pages/_app.tsx"
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
// highlight-next-line
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
function MyApp({
Component,
pageProps,
}: AppProps<{
// highlight-next-line
initialSession: Session,
}>) {
// Create a new supabase browser client on every first render.
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
</TabItem>
</Tabs>
You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined.
## Usage with TypeScript
You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
```js
// Creating a new supabase client object:
import { Database } from '../db_types';
### Browser client
const [supabaseClient] = useState(() =>
createBrowserSupabaseClient<Database>()
);
```ts
// Creating a new supabase client object:
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { Database } from '../database.types'
const supabaseClient = createBrowserSupabaseClient<Database>()
```
```js
```ts
// Retrieving a supabase client object from the SessionContext:
import { useSupabaseClient } from '@supabase/auth-helpers-react';
import { Database } from '../db_types';
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { Database } from '../database.types'
const supabaseClient = useSupabaseClient<Database>();
const supabaseClient = useSupabaseClient<Database>()
```
### Server client
```ts
// Creating a new supabase server client object (e.g. in API route):
import type { NextApiRequest, NextApiResponse } from 'next'
import type { Database } from 'types_db'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const supabaseServerClient = createServerSupabaseClient<Database>({
req,
res,
})
const {
data: { user },
} = await supabaseServerClient.auth.getUser()
res.status(200).json({ name: user?.name ?? '' })
}
```
## Client-side data fetching with RLS
For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSessionContext` hook and only run your query once the user is defined client-side in the `useUser()` hook:
For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook:
```js
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
import { useUser, useSessionContext } from '@supabase/auth-helpers-react'
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
import { useEffect, useState } from 'react'
const LoginPage = () => {
const { isLoading, session, error, supabaseClient } = useSessionContext()
const supabaseClient = useSupabaseClient()
const user = useUser()
const [data, setData] = useState()
// highlight-start
useEffect(() => {
async function loadData() {
const { data } = await supabaseClient.from('test').select('*')
@@ -145,19 +200,17 @@ const LoginPage = () => {
// Only run query once user is logged in.
if (user) loadData()
}, [user])
// highlight-end
if (!user)
return (
<>
{error && <p>{error.message}</p>}
<Auth
redirectTo="http://localhost:3000/"
appearance={{ theme: ThemeSupa }}
supabaseClient={supabaseClient}
providers={['google', 'github']}
socialLayout="horizontal"
/>
</>
<Auth
redirectTo="http://localhost:3000/"
appearance={{ theme: ThemeSupa }}
supabaseClient={supabaseClient}
providers={['google', 'github']}
socialLayout="horizontal"
/>
)
return (
@@ -174,7 +227,7 @@ const LoginPage = () => {
export default LoginPage
```
### Server-side rendering (SSR) - withPageAuth
## Server-side rendering (SSR) - withPageAuth
If you wrap your `getServerSideProps` with `withPageAuth` your props object will be augmented with the user object.
@@ -191,7 +244,7 @@ export const getServerSideProps = withPageAuth({ redirectTo: '/login' })
If there is no authenticated user, they will be redirect to your home page, unless you specify the `redirectTo` option.
You can pass in your own `getServerSideProps` method, the props returned from this will be merged with the
user props. You can also access the user session data by calling `getUser` inside of this method, eg:
user props. You can also access the user session data by calling `supabase.auth.getUser()` inside of this method, eg:
```js title="pages/protected-page.js"
import { withPageAuth } from '@supabase/auth-helpers-nextjs'
@@ -212,7 +265,7 @@ export const getServerSideProps = withPageAuth({
})
```
### Server-side data fetching with RLS
## Server-side data fetching with RLS
Both `withApiAuth` and `withPageAuth` return a supabase client that you can use to run [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side:
@@ -245,44 +298,62 @@ export const getServerSideProps = withPageAuth({
})
```
### Server-side data fetching to OAuth APIs using `provider_token`
## Server-side data fetching to OAuth APIs using `provider_token`
When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted as an HTTPOnly cookie upon logging in to enabled usage on the server side. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user. In the following example, we fetch the user's full profile from the third-party API during SSR using their id and auth token:
```js
import { User, withPageAuth, getUser } from '@supabase/auth-helpers-nextjs'
interface Profile {
/* ... */
}
import { User, withPageAuth } from '@supabase/auth-helpers-nextjs'
export default function ProtectedPage({
user,
data,
allRepos,
}: {
user: User,
profile: Profile,
allRepos: any,
}) {
return <div>Protected content</div>
return (
<>
<div>Protected content for {user.email}</div>
<p>Data fetched with provider token:</p>
<pre>{JSON.stringify(allRepos, null, 2)}</pre>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = withPageAuth({
redirectTo: '/',
async getServerSideProps(ctx) {
// Retrieve provider_token from cookies
const provider_token = ctx.req.cookies['sb-provider-token']
// Get logged in user's third-party id from metadata
const { user } = await getUser(ctx)
const userId = user?.user_metadata.provider_id
const profile: Profile = await (
await fetch(`https://api.example.com/users/${userId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${provider_token}`,
},
})
async getServerSideProps(ctx, supabase) {
const {
data: { session },
error,
} = await supabase.auth.getSession()
if (error) {
throw error
}
if (!session) {
return { props: {} }
}
// Retrieve provider_token & logged in user's third-party id from metadata
const { provider_token, user } = session
const userId = user.user_metadata.user_name
const allRepos = await (
await fetch(
`https://api.github.com/search/repositories?q=user:${userId}`,
{
method: 'GET',
headers: {
Authorization: `token ${provider_token}`,
},
}
)
).json()
return { props: { profile } }
return { props: { allRepos, user } }
},
})
```
@@ -355,6 +426,7 @@ export const config = {
</button>
```
- The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createBrowserSupabaseClient` and `createServerSupabaseClient` methods. This allows you to provide the CLI-generated types to the client:
```js
// client-side
import type { Database } from 'types_db';
@@ -373,12 +445,14 @@ export const config = {
res.status(200).json({ name: user?.name ?? '' })
}
```
- The `UserProvider` has been replaced by the `SessionContextProvider`. Make sure to wrap your `pages/_app.js` componenent with the `SessionContextProvider`. Then, throughout your application you can use the `useSessionContext` hook to get the `session` and the `useSupabaseClient` hook to get an authenticated `supabaseClient`.
- The `useUser` hook now returns the `user` object or `null`.
- Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
```js
// Creating a new supabase client object:
import { Database } from '../db_types';
import { Database } from '../database.types';
const [supabaseClient] = useState(() =>
createBrowserSupabaseClient<Database>()
@@ -388,7 +462,7 @@ export const config = {
```js
// Retrieving a supabase client object from the SessionContext:
import { useSupabaseClient } from '@supabase/auth-helpers-react';
import { Database } from '../db_types';
import { Database } from '../database.types';
const supabaseClient = useSupabaseClient<Database>();
```
@@ -65,12 +65,14 @@ async function signInWithWorkOS() {
const { data, error } = await supabase.auth.signInWithOAuth(
{
provider: 'workos',
options: {
queryParams: {
connection: '<your_connection>',
organization: '<your_organization',
provider: '<your_provider>',
}
}
},
{
connection: '<your_connection>',
organization: '<your_organization',
provider: '<your_provider>',
}
)
}
```
@@ -0,0 +1,202 @@
---
id: server-side-rendering
title: Server-Side Rendering
description: Render pages with user information on the server.
---
Single-page apps with server-side rendering (SSR) is a popular way to optimize rendering
performance and leverage advanced caching strategies.
Supabase Auth supports server-side rendering when you need access to user
information, or your server needs to authorize API requests on behalf of your
user to render content.
When a user authenticates with Supabase Auth, two pieces of information are
issued by the server:
1. **Access token** in the form of a JWT.
2. **Refresh token** which is a randomly generated string.
Most Supabase projects have their auth server listening on
`<project-ref>.supabase.co/auth/v1`, thus the access token and refresh token are
set as `sb-access-token` and `sb-refresh-token` cookies on the
`<project-ref>.supabase.co` domain.
:::note
These cookie names are for internal Supabase use only and may change without
warning. They are included in this guide for illustration purposes only.
:::
Web browsers limit access to cookies across domains, consistent with the
[Same-Origin Policy
(SOP)](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy).
Your web application cannot access these cookies,
nor will these cookies be sent to your application's server.
## Understanding the authentication flow
When you call one of the `signIn` methods, the client library running in the
browser sends the request to the Supabase Auth server. The Auth server determines
whether to verify a phone number, email and password combination, a Magic Link,
or use a social login (if you have any setup in your project).
Upon successful verification of the identity of the user, the Supabase Auth
server redirects the user back to your single-page app.
:::tip
You can configure [redirects URLs](https://app.supabase.com/project/_/auth/settings) in the Supabase Dashboard. You can use wildcard match patterns
like `*` and `**` to allow redirects to different forms of URLs.
:::
These redirect URLs have the following structure:
```
https://yourapp.com/...#access_token=<...>&refresh_token=<...>&...
```
The first access and refresh tokens after a successful verification are
contained in the URL fragment (anything after the `#` sign) of the redirect
location. This is intentional and not configurable.
The client libraries are designed to listen for this type of URL, extract
the access token, refresh token and some extra information from it, and finally
persist it in local storage for further use by the library and your app.
:::info
Web browsers do not send the URL fragment to the server they're making the
request to. Since you may not be hosting the single-page app on a server under
your direct control (such as on GitHub Pages or other freemium hosting
providers), we want to prevent hosting services from getting access to your
user's authorization credentials by default. Even if the server is under your
direct control, `GET` requests and their full URLs are often logged. This
approach also avoids leaking credentials in request or access logs.
:::
## Bringing it together
As seen from the authentication flow, the initial request after successful
login made by the browser to your app's server after user login **does not
contain any information about the user**. This is because first the client-side
JavaScript library must run before it makes the access and refresh token
available to your server.
It is very important to make sure that the redirect route right after login
works without any server-side rendering. Other routes requiring authorization
do not have the same limitation, provided you send the access and refresh
tokens to your server.
This is traditionally done by setting cookies. Here's an example you
can add to the root of your application:
```typescript
supabase.auth.onAuthStateChange((event, session) => {
if (event === "SIGNED_OUT" || event === "USER_DELETED") {
// delete cookies on sign out
const expires = new Date(0).toUTCString();
document.cookie = `my-access-token=; path=/; expires=${expires}; SameSite=Lax; secure`;
document.cookie = `my-refresh-token=; path=/; expires=${expires}; SameSite=Lax; secure`;
} else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
const maxAge = 100 * 365 * 24 * 60 * 60; // 100 years, never expires
document.cookie = `my-access-token=${session.access_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure`;
document.cookie = `my-refresh-token=${session.refresh_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure`;
}
})
```
This uses the standard
[`document.cookie` API](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie)
to set cookies on all paths of your app's domain. All subsequent requests
made by the browser to your app's server include the `my-access-token` and
`my-refresh-token` cookies (the names of the cookies and additional
parameters can be changed).
In your server-side rendering code you can now access user and session
information:
```typescript
const refreshToken = req.cookies['my-refresh-token'];
const accessToken = req.cookies['my-access-token'];
if (refreshToken && accessToken) {
await supabase.auth.setSession({
refresh_token: refreshToken,
access_token: accessToken
});
} else {
// make sure you handle this case!
throw new Error("User is not authenticated.")
}
// returns user information
await supabase.auth.getUser();
```
Use `setSession({ access_token, refresh_token })` instead of
`setSession(refreshToken)` or `getUser(accessToken)` as refresh tokens or access tokens alone do not properly identify a user session.
Access tokens are valid only for a short amount of time.
Even though refresh tokens are long-lived, there is no guarantee that a user
has an active session. They may have logged out and your application failed to
remove the `my-refresh-token` cookie, or some other failure occurred that left
a stale refresh token in the browser. Furthermore, a refresh token can only be
used a few seconds after it was first used. Only use a refresh token if the
access token is about to expire, which will avoid the introduction of difficult
to diagnose logout bugs in your app.
A good practice is to handle unauthorized errors by deferring rendering the
page in the browser instead of in the server. Some user information is
contained in the access token though, so in certain cases, you may be able to
use this potentially stale information to render a page.
## Frequently Asked Questions
### How do I make the cookies `HttpOnly`?
This is not necessary. Both the access token and refresh token are designed to
be passed around to different components in your application. The browser-based
side of your application needs access to the refresh token to properly maintain
a browser session anyway.
### My server is getting invalid refresh token errors. What's going on?
It is likely that the refresh token sent from the browser to your server is
stale. Make sure the `onAuthStateChange` listener callback is free of bugs and
is registered relatively early in your application's lifetime.
When you receive this error on the server-side, try to defer
rendering to the browser where the client library can access an up-to-date
refresh token and present the user with a better experience.
### Should I set a shorter `Max-Age` parameter on the cookies?
The `Max-Age` or `Expires` cookie parameters only control whether the browser
sends the value to the server. Since a refresh token represents the
long-lived authentication session of the user on that browser, setting a short
`Max-Age` or `Expires` parameter on the cookies only results in a degraded
user experience.
The only way to ensure that a user has logged out or their session has ended
is to get the user's details with `getUser()`.
### What should I use for the `SameSite` property?
Make sure you [understand the behavior of the property in different
situations](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
as some properties can degrade the user experience.
A good default is to use `Lax` which sends cookies when users are
navigating to your site. Cookies typically require the `Secure` attribute,
which only sends them over HTTPS. However, this can be a problem when
developing on `localhost`.
### Can I use server-side rendering with a CDN or cache?
Yes, but you need to be careful to include at least the refresh token cookie
value in the cache key. Otherwise you may be accidentally serving pages with
data belonging to different users!
Also be sure you set proper cache control headers. We recommend invalidating
cache keys every hour or less.
@@ -348,6 +348,24 @@ Once pushed, check that the migration version is up to date for both local and r
supabase migration list
```
### Permission denied on db remote commit
If you have been using Supabase hosted projects for a long time, you might encounter the following permission error when executing `db remote commit`.
```bash
Error: Error running pg_dump on remote database: pg_dump: error: query failed: ERROR: permission denied for table _type
pg_dump: error: query was: LOCK TABLE "graphql"."_type" IN ACCESS SHARE MODE
```
To resolve this error, you need to grant `postgres` role permissions to `graphql` schema. You can do that by running the following query from Supabase dashboard's SQL Editor.
```sql
grant all on all tables in schema graphql to postgres, anon, authenticated, service_role;
grant all on all functions in schema graphql to postgres, anon, authenticated, service_role;
grant all on all sequences in schema graphql to postgres, anon, authenticated, service_role;
```
### Permission denied on db push
If you created a table through Supabase dashboard, and your new migration script contains `ALTER TABLE` statements, you might run into permission error when applying them on staging or production databases.
@@ -358,12 +376,14 @@ ERROR: must be owner of table employees (SQLSTATE 42501); while executing migrat
This is because tables created through Supabase dashboard are owned by `supabase_admin` role while the migration scripts executed through CLI are under `postgres` role.
One way to solve this is to grant `postgres` role additional privileges through the SQL Editor available on Supabase dashboard. For example, the following command grants postgres permissions to alter any table in the public schema.
One way to solve this is to reassign the owner of those tables to `postgres` role. For example, if your table is named `users` in the public schema, you can run the following command to reassign owner.
```sql
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres;
ALTER TABLE users OWNER TO postgres;
```
Apart from tables, you also need to reassign owner of other entities using their respective commands, including [types](https://www.postgresql.org/docs/current/sql-alterschema.html), [functions](https://www.postgresql.org/docs/current/sql-alterroutine.html), and [schemas](https://www.postgresql.org/docs/current/sql-altertype.html).
### Rebasing new migrations
Sometimes your teammate may merge a new migration file to git main branch, and now you need to rebase your local schema changes on top.
+171
View File
@@ -0,0 +1,171 @@
---
id: features
title: Features
description: 'Supabase features'
---
This is a non-exhaustive list of features that Supabase provides for every project.
## Database
### Postgres Database
Every project is a full Postgres database. [Docs](/docs/guides/database).
### Database Extensions
Every database comes with a full set of Postgres extensions. [Docs](/docs/guides/database/extensions).
### Database Functions
Create custom database functions which you can call from the browser. [Docs](/docs/guides/database/functions).
### Database Triggers
Attach triggers to your tables to handle database changes. [Docs](/docs/guides/auth/managing-user-data#using-triggers).
### Database Webhooks
Send database changes to any external service using Webhooks. [Link](https://supabase.com/blog/supabase-functions-updates#database-webhooks-alpha).
### Database Backups
Projects are backed up daily with the option to upgrade to Point in Time recovery.
### Search
Build search functionality using Postgres Full Text Search. [Docs](/docs/guides/database/full-text-search).
### Secrets and encryption
Encrypt sensitive data and store secrets using our Postgres extension, Supabase Vault. [Link](https://supabase.com/blog/supabase-vault).
### Database migrations
Develop locally and push your changes to your production database using migrations. [Docs](/docs/guides/cli/local-development#database-migrations)
<br />
## Auth
### Email & Password Logins
Build email logins for your application or website. [Docs](/docs/guides/auth/auth-email).
### Magic Links
Build passwordless logins for your application or website.[Docs](/docs/guides/auth/auth-magic-link).
### Social Logins
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/auth-apple).
### Phone Logins
Provide phone logins using a 3rd-party SMS provider. [Docs](/docs/guides/auth/auth-twilio).
### Row Level Security
Control the data each user can access with Postgres Policies. [Docs](/docs/guides/auth/row-level-security).
### Serverside Auth Helpers
Helpers for implementing user authentication in popular frameworks like [Next.js](/docs/guides/auth/auth-helpers/nextjs) and [SvelteKit](/docs/guides/auth/auth-helpers/sveltekit)
### Auth UI Kit
Build login and registration pages with custom themes. [Docs](/docs/guides/auth/auth-helpers/auth-ui).
<br />
## APIs & Client libraries
### Auto-generated REST API
RESTful APIs are autogenerated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
### Auto-generated GraphQL API
Fast GraphQL APIs using our custom Postgres GraphQL extension. [Docs](/docs/guides/api#graphql-api-overview).
### Realtime Database changes
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-cdc).
### User Broadcasting
Send messages between connected users through websockets. [Docs](/docs/guides/realtime#broadcast).
### User Presence
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime#presence).
### Client libraries
Official client libraries for [JavaScript](/docs/reference/javascript/) and [Dart](/docs/reference/dart).
Unofficial libraries [supported by the community](https://github.com/supabase-community#client-libraries).
<br />
## File Storage
### Large File storage
Supabase Storage makes it simple to store and serve large files. [Docs](/docs/guides/storage).
### Storage CDN
Cache large files using the Supabase CDN. [Docs](/docs/guides/storage-cdn).
<br />
## Edge Functions
### Deno Edge Functions
Globally distributed TypeScript functions to execute custom business logic. [Docs](/docs/guides/functions).
<br />
## Project Managment
### CLI
Use our CLI to develop your project locally and deploy to the Supabase Platform. [Docs](/docs/reference/cli).
### Admin API
Manage your projects programmatically. [Docs](/docs/reference/api).
<br />
## Feature Status
Both Postgres and the Supabase Platform are production-ready. Some tools we offer on top of Postgres are still under development.
| Product | Feature | Stage |
| -------------------------- | ---------------------- | ------- |
| Database | Postgres | `GA` |
| Database | Triggers | `GA` |
| Database | Functions | `GA` |
| Database | Extensions | `GA` |
| Database | Full Text Search | `GA` |
| Database | Webhooks | `alpha` |
| Database | Point-in-Time Recovery | `alpha` |
| Database | Vault | `alpha` |
| Studio | | `GA` |
| Realtime | Postgres CDC | `beta` |
| Realtime | Broadcast | `alpha` |
| Realtime | Presence | `alpha` |
| Storage | Backend (S3) | `GA` |
| Storage | API | `beta` |
| Storage | CDN | `beta` |
| Edge Functions | | `beta` |
| Auth | OAuth Providers | `beta` |
| Auth | Passwordless | `beta` |
| Auth | Next.js Auth Helpers | `alpha` |
| Auth | SvelteKit Auth Helpers | `alpha` |
| Admin API | | `beta` |
| CLI | | `beta` |
| Client Library: JavaScript | | `GA` |
| Client Library: Dart | | `beta` |
@@ -0,0 +1,175 @@
---
id: dhiwise
title: 'DhiWise'
description: 'Get started with Supabase and DhiWise. Convert your Figma designs into Flutter apps, store data, and authenticate your users'
---
import select from "/img/guides/integrations/dhiwise/select.png"
import create from "/img/guides/integrations/dhiwise/create.png"
This guide explains how to connect Supabase backend to DhiWise Flutter application quickly.
[DhiWise](https://www.dhiwise.com/) is a Developer tool to convert Figma designs into React and Flutter applications. It lets you quickly integrate Databases and APIs into your React and Flutter Apps.
If you don't have a DhiWise account, create one [here](https://app.dhiwise.com).
DhiWise supports easy Supabase Integration in just five steps.
Let's get started!
## Step 1: SignIn to Supabase
Go to [Supabase](https://supabase.com/), Click `Sign In`, and create a new account by authenticating with **GitHub**. If you already have an account, you will be logged in.
## Step 2: Create a new project in Supabase
Click on `New project` from the Dashboard and select an organization. If you don't have an organization, create one using `+ New organization.`
- Give your Supabase project a `name.`
- Enter a secure `Database Password.`
- Choose the `region` where your app's backend is hosted.
- Click `Create new project.`
![New Project](/img/guides/integrations/dhiwise/newProj.png)
## Step 3: Find the API key and URL
Once your project is created, you can access the API Key and URL string, Or if you already have an account go to your `organization-> app-> settings-> API`.
![auth keys](/img/guides/integrations/dhiwise/authKeys.png)
## Step 4: Integrations
There are two ways you can integrate Supabase into your DhiWise Flutter applications.
### Authentication
You can integrate `Supabase Email/Password SignUp` or `Supabase Email/Password SignIn` on your components.
- Open the screen of your flutter application
- Go to the component on which you want to add authentication
- on the `onClick` method - select `authentication`
- From the list, If you want SignUp - select `SignUp with Email/Password`; otherwise, select `SignIn with Email/Password` from Supabase Auth section
![Auth](/img/guides/integrations/dhiwise/auth.gif)
And that's it. Supabase authentication will be added to the selected component.
After downloading the application source code,
1. Add Supabase URL and Supabase public key inside ***lib/core/utils/initial_bindings*** file.
2. For additional details, refer ***https://supabase.com/docs/guides/with-flutter***
### Working with Data
When you first integrate Supabase in your DhiWise Flutter application, You will be asked to add [Supabase auth key and URL](##step-3-find-the-api-key-and-url). When you add them, all the tables available in your Supabase project will be synced in DhiWise. You can integrate Select and Create queries on your Flutter screen for a particular table in DhiWise.
<h3> Select records </h3>
#### **Step 1:**
Select the screen from the screen list where you want to integrate Supabase.
#### **Step 2:**
Next, go to the view where you want to add Integration, and from the suggestion box for the `onClick` property, choose `Supabase integration,` which will take you to the Integration screen. Where you will be asked to `Enter function name.` Enter the name of your function and click `Submit.`
#### **Step 3:**
After submitting the function name, you will be asked to select a type of Supabase integration. To retrieve data from Supabase, choose `select.`
#### **Step 4:**
Next, select the table from which you want to fetch records from the listed Tables.
#### **Step 5:**
Select the type of integration
| Type | Description |
| --- | --- |
| **Single** | Used to fetch a single record from the database. |
| **Multiple** | Used to fetch multiple records from the database. |
:::info
For Multiple types, you need to set `data limit,` `order by, and `order.`
:::
#### **Step 6:**
You will be redirected to the API Integration screen, where you can set request and response.
For request binding, the below types are supported. Also, Select the operator for comparison before moving forward.
| Type | Description |
| --- | --- |
| **View** | Select any component from your screen. |
| **Constant** | Select a constant you've created in your app. |
| **Get from preference** | Select the key you want to fetch from preference. |
| **Navigation argument** | Select data that's been passed from one screen to another. |
For response binding, the below types are supported.
| Type | Description |
| --- | --- |
| **View** | Select any component from the screen. |
| **Save to preference** | Storing the data to preference. |
#### **Select 7:**
`Handle action` - Select the action you wish to take once the Supabase call has either been accepted successfully or refused due to an error.
Available action for On success and On error are,
1. [Show Alert](https://docs.dhiwise.com/docs/flutter/show-alert)
2. [Navigation](https://docs.dhiwise.com/docs/flutter/navigation)
#### **Step 8:**
Finally, you have added Supabase to your application to fetch records on your screen!
:::tip Example
Suppose you want to fetch records from Supabase and populate the item list on your screen. You can integrate Supabase as discussed above and bind the response with your list view.
:::
<h3> Create records </h3>
#### **Step 1:**
Choose the screen you wish to integrate Supabase for from the list of screens.
#### **Step 2:**
Next, switch to the component you want to add Integration, and on the `onClick` property, choose `Supabase integration,` which will take you to its integration screen, where you will be asked to **Enter function name**, which will be used in generated code. Enter the name for it and click `Submit`
#### **Step 3:**
After submitting the function name, you will be asked to select a type of Supabase integration. For example, to create a record in Supabase, choose `Create.`
#### **Step 4:**
Next, select the table where you want to create a record from the listed Tables.
#### **Step 5:**
If you want to create a Single record, Select **Select**. Otherwise, **Multiple**.
#### **Step 6:**
Now, you will be redirected to the API Integration screen, where you can set request and response.
For request binding, the below types are supported.
| Type | Description |
| --- | --- |
| **View** | Select any component from the screen |
| **Constant** | Select a constant you've created in your app.|
| **Get from preference** | Select the key you want to fetch from preference. |
| **Navigation argument** | Select data that's been passed from one screen to another. |
For response binding, the below types are supported.
| Type | Description |
| --- | --- |
| **View** | Select any component from the screen |
| **Save to preference** | Storing the data to preference. |
#### **Select 7:**
`Handle action` - Select the action you wish to take once the Supabase call has either been accepted successfully or refused due to an error.
Available action for On success and On error are,
1. [Show Alert](https://docs.dhiwise.com/docs/flutter/show-alert)
2. [Navigation](https://docs.dhiwise.com/docs/flutter/navigation)
#### **Step 9:**
Finally, you have added Supabase to your application to create records from your screen data!
## Resources
- [DhiWise Official Website](https://dhiwise.com)
- [DhiWise Documentation](https://docs.dhiwise.com)
@@ -0,0 +1,34 @@
---
id: integrations
title: 'Supabase Marketplace'
description: 'Integrations and Partners'
---
The Supabase Marketplace brings together all the tools you need to extend your Supabase project. This includes:
- [Experts](https://supabase.com/partners/experts) - partners to help you build and support your Supabase project.
- [Integrations](https://supabase.com/partners/integrations) - extend your projects with external Auth, Caching, Hosting, and Low-code tools.
## Build an integration
Supabase provides several integration points:
- The [Postgres connection](/docs/guides/database/connecting-to-postgres). Anything that works with Postgres also works with Supabase projects.
- The [Project REST API](/docs/guides/api#rest-api) & client libraries.
- The [Project GraphQL API](/docs/guides/api#graphql-api).
- The [Platform API](/docs/reference/api).
## List your integration
[Apply to the Partners program](https://supabase.com/partners/integrations#become-a-partner) to list your integration in the Partners marketplace and in the Supabase docs.
Integrations are assessed on the following criteria:
- **Business viability**
While we welcome everyone to built an integration, we only list companies that are deemed to be long-term viable. This includes an official business registration and bank account, meaningful revenue, or Venture Capital backing. We require this criteria to ensure the health of the marketplace.
- **Compliance**
Integrations should not infringe on the Supabase brand/trademark. In short, you cannot use "Supabase" in the name. As the listing appears on the Supabase domain, we don't want to mislead developers into thinking that an integration is an official product.
- **Service Level Agreements**
All listings are required to have their own Terms and Conditions, Privacy Policy, and Acceptable Use Policy, and the company must have resources to meet their SLAs.
- **Maintainability**
All integrations are required to be maintained and functional with Supabase, and the company may be assessed on your ability to remain functional over a long time horizon.
@@ -0,0 +1,149 @@
​
id: keyri
title: 'Keyri'
description: 'QR authentication for an easy and flexible biometric solution across all platforms.'
---
​
Keyri can be used to incorporate sign-in-with-QR functionality into your Supabase app, allowing users to scan a QR code on your web app with your mobile app and be instantly logged into the web app without having to input any credentials.
​
Configuration is split into Web and Mobile components. On web, the Keyri QR Widget needs to be installed along with an event listener, and in your mobile app, install the Keyri SDK and pass into it the user's refresh token when sign-in-with-QR is initiated. When the refresh token lands in your web app, it's passed into Supabase's `setSession()` method.
​
# Sign up for Keyri
​
First make a free account on the Keyri dashboard (<https://app.keyri.com>). On Add Your Application, set a name and input the domain on which your app will eventually be deployed. You can create multiple application in Keyri to account for your development, staging, and production environments
​
![](https://archbee-image-uploads.s3.amazonaws.com/FQ4YmCkDokMJylbTAsoOR/HvTIja3KfgKUIMiNVKAqP_screen-shot-2022-10-13-at-21524-pm.png)
​
Note your application key from the Keys and Credentials section - this will be used in the Mobile portion of the implementation
​
![](https://archbee-image-uploads.s3.amazonaws.com/FQ4YmCkDokMJylbTAsoOR/KnD6LkWs-PUDtTS1sT9Rz_screen-shot-2022-10-13-at-21746-pm.png)
​
# Web
[![Keyri-Supabase Web Integration Video](http://img.youtube.com/vi/jrjrcpc2PFQ/0.jpg)](http://www.youtube.com/watch?v=jrjrcpc2PFQ "Supabase & Keyri | Web Integration Overview")
​
For your web app, first download KeyriQR.html (available [here](https://raw.githubusercontent.com/Keyri-Co/library-keyri-connect/main/KeyriQR.html)) and save it to a public directory.
​
Next, embed KeyriQR.html in your login page as an iFrame within the desired div. This serves as the widget that displays the dynamic QR code and connects with the Keyri API.
​
```html
<div>
<iframe
title='KeyriQR'
src='/KeyriQR.html'
id='qr-frame'
height='300'
width='300'
scrolling='no'
style={{ border: 'solid 5px white' }}
></iframe>
</div>
```
​
Next, for the same login view, set up an event listener to pick up the session token that the iFrame emits when the QR code is scanned by your app.
​
```javascript
useEffect(() => {
window.addEventListener('message', async (evt) => {
if (evt.data.keyri && evt.data.data && document.location.origin == evt.origin) {
const { data } = evt;
if (!data.error) {
let refresh_token = JSON.parse(data.data).refreshToken;
await handleQrLogin(refresh_token);
} else if (data.error) {
console.log(`Keyri error: ${data.message}`);
}
}
});
```
​
That's it!
​
# Mobile
[![Keyri-Supabase Mobile Integration Video](http://img.youtube.com/vi/oGMsSKyh6tc/0.jpg)](http://www.youtube.com/watch?v=oGMsSKyh6tc "Supabase & Keyri | Mobile Integration Overview")
​
### Install Flutter
​
First, install the Flutter SDK, found at flutter.dev
​
Make sure to add Flutter to your PATH, for example:&#x20;
​
```shell
export PATH="$PATH:`pwd`/flutter/bin"
```
​
### Apple - initial setup
​
Download the latest version of Xcode from the Mac App Store. Make sure the Xcode provided simulator is using a 64-bit device (iPhone 5s or later). You can check the device by viewing the settings in the simulator’s **Hardware > Device** or **File > Open Simulator** menus.
​
### Android - initial setup
​
Download the latest version of [Android Studio](https://developer.android.com/studio). Install Android SDK and needed emulator(s).
​
### Create Project
​
Run this command in your terminal/shell at the desired location for your new project
​
```shell
$ flutter create my_app
```
​
You can then CD into the new directory, and run the test app with&#x20;
​
```shell
flutter run
```
​
This is a good test - if things are configured correctly so far you should see the default Flutter test app deployed.
​
### Add dependencies (Keyri and Supabase)
​
Open your Pubspec.yaml file, which should be at the top level directory in your new project
​
Add Keyri and Supabase under **dependencies**
​
![](https://archbee-image-uploads.s3.amazonaws.com/FQ4YmCkDokMJylbTAsoOR/jlAfOTEchuZpBq8TeXhJZ_screen-shot-2022-09-29-at-060908.png)
​
One can now access Supabase and Keyri sdks in their Flutter code
​
### Utilize the two together
​
1. Make a request to Supabase to authenticate the user
​
2. Parse the response to extract the token
​
3. Authenticate using Keyri
1. Below, we show how to utilize the EasyKeyriAuth function, which takes the user through scanning the code, creating the session, displaying the confirmation screen, and finalizing the payload transmission
- Note - you can find your App Key in the Keyri Developer Portal
​
2. Alternatively, intermediate functions in the Keyri SDK, discussed in the mobile docs, can provide control over displaying a custom QR Scanner and/or Confirmation screen
​
```kotlin
// Sign in user with email and password
// Alternatively one can utilize the Supabase API to accomplish the same thing
final response = await client.auth.signIn(email: 'email', password: 'password');
​
if (response.error != null) {
// Error
print('Error: ${response.error?.message}');
} else {
// Success
final session = response.data;
​
// This is the payload that needs to be send through Keyri
final refreshToken = session.refreshToken
​
// EasyKeyriAuth guides the user through scanning and parsing the QR, confirming the session, and configuring the payload
// One can also use the initiateQRSession method to use the Keyri Scanner with a custom Confirmation screen
// Or the ProcessLink method if you have your own scanner or are using deep linking
await keyri
.easyKeyriAuth([App Key],
'{"refreshToken":"$refreshToken"}', [email])
.then((authResult) => _onAuthResult(authResult))
.catchError((error, stackTrace) => _onError(error));
​
}
​
​
```
@@ -103,7 +103,7 @@ const channel = supabase.channel('calc-latency', {
})
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED) {
if (status === 'SUBSCRIBED') {
const begin = performance.now()
await channel.send({
+479 -175
View File
@@ -61,6 +61,7 @@ Now we are going to set up the database schema. We can use the "User Management
or you can just copy/paste the SQL from below and run it yourself.
<Tabs
groupId="language"
defaultValue="dashboard"
values={[
{label: 'Dashboard', value: 'dashboard'},
@@ -85,51 +86,59 @@ or you can just copy/paste the SQL from below and run it yourself.
<TabItem value="sql">
```sql
-- Create a table for public "profiles"
-- Create a table for public profiles
create table profiles (
id uuid references auth.users not null,
id uuid references auth.users not null primary key,
updated_at timestamp with time zone,
username text unique,
full_name text,
avatar_url text,
website text,
primary key (id),
unique(username),
constraint username_length check (char_length(username) >= 3)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
alter table profiles
enable row level security;
alter table profiles enable row level security;
create policy "Public profiles are viewable by everyone." on profiles
for select using (true);
create policy "Public profiles are viewable by everyone."
on profiles for select
using ( true );
create policy "Users can insert their own profile." on profiles
for insert with check (auth.uid() = id);
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile." on profiles
for update using (auth.uid() = id);
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
-- Set up Realtime!
begin;
drop publication if exists supabase_realtime;
create publication supabase_realtime;
commit;
alter publication supabase_realtime add table profiles;
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
values ('avatars', 'avatars');
create policy "Avatar images are publicly accessible."
on storage.objects for select
using ( bucket_id = 'avatars' );
-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
for select using (bucket_id = 'avatars');
create policy "Anyone can upload an avatar."
on storage.objects for insert
with check ( bucket_id = 'avatars' );
create policy "Anyone can upload an avatar." on storage.objects
for insert with check (bucket_id = 'avatars');
create policy "Anyone can update their own avatar." on storage.objects
for update using ( auth.uid() = owner ) with check (bucket_id = 'avatars');
```
@@ -163,12 +172,32 @@ Let's start building the Next.js app from scratch.
We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize
an app called `supabase-nextjs`:
<Tabs
groupId="language"
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
```bash
npx create-next-app supabase-nextjs --use-npm
npx create-next-app@latest --use-npm supabase-nextjs
cd supabase-nextjs
```
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
</TabItem>
<TabItem value="ts">
```bash
npx create-next-app@latest --ts --use-npm supabase-nextjs
cd supabase-nextjs
```
</TabItem>
</Tabs>
Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js
@@ -182,93 +211,148 @@ NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client.
These variables will be exposed on the browser, and that's completely fine since we have
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
```js title="utils/supabaseClient.js"
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
```
And one optional step is to update the CSS file `styles/globals.css` to make the app look nice.
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/nextjs-ts-user-management/styles/globals.css).
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-ts-user-management/styles/globals.css).
### Set up a Login component
Let's set up a React component to manage logins and sign ups.
We'll use Magic Links, so users can sign in with their email without using passwords.
#### Supabase Auth Helpers
```jsx title="components/Auth.js"
import { useState } from 'react'
import { supabase } from '../utils/supabaseClient'
Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions.
export default function Auth() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
It can be challenging to authenticate your users in all these different environments, that's why we've created the [Supabase Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) to make user management and data fetching within Next.js as easy as possible.
const handleLogin = async (email) => {
try {
setLoading(true)
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
alert(error.error_description || error.message)
} finally {
setLoading(false)
}
}
Install the auth helpers for React and Next.js
```bash
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
```
<Tabs
groupId="language"
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
```jsx title="pages/_app.js"
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
function MyApp({ Component, pageProps }) {
const router = useRouter()
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
return (
<div className="row flex-center flex">
<div className="col-6 form-widget">
<h1 className="header">Supabase + Next.js</h1>
<p className="description">
Sign in via magic link with your email below
</p>
<div>
<input
className="inputField"
type="email"
placeholder="Your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<button
onClick={(e) => {
e.preventDefault()
handleLogin(email)
}}
className="button block"
disabled={loading}
>
<span>{loading ? 'Loading' : 'Send magic link'}</span>
</button>
</div>
</div>
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
</TabItem>
<TabItem value="ts">
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
```jsx title="pages/_app.tsx"
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
// highlight-next-line
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
function MyApp({
Component,
pageProps,
}: AppProps<{
// highlight-next-line
initialSession: Session,
}>) {
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
See the [Auth Helpers docs](/docs/guides/auth/auth-helpers/nextjs#usage-with-typescript) for more details on usage with TypeScript.
</TabItem>
</Tabs>
#### Supabase Auth UI
We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links.
Install the Supabase Auth UI for React
```bash
npm install @supabase/auth-ui-react
```
Add the `Auth` component to your home page
```jsx title="pages/index.js"
import type { NextPage } from 'next'
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
const Home: NextPage = () => {
const session = useSession()
const supabase = useSupabaseClient()
return (
<div className="container" style={{ padding: '50px 0 100px 0' }}>
{!session ? (
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme="dark"
/>
) : (
<p>Account page will go here.</p>
)}
</div>
)
}
export default Home
```
### Account page
After a user is signed in we can allow them to edit their profile details and manage their account.
Let's create a new component for that called `Account.js`.
<Tabs
groupId="language"
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
```jsx title="components/Account.js"
Let's create a new component for that called `Account.js` within a `components` folder.
```tsx title="components/Account.js"
import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabaseClient'
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
export default function Account({ session }) {
const supabase = useSupabaseClient()
const user = useUser()
const [loading, setLoading] = useState(true)
const [username, setUsername] = useState(null)
const [website, setWebsite] = useState(null)
@@ -278,27 +362,9 @@ export default function Account({ session }) {
getProfile()
}, [session])
async function getCurrentUser() {
const {
data: { session },
error,
} = await supabase.auth.getSession()
if (error) {
throw error
}
if (!session?.user) {
throw new Error('User not logged in')
}
return session.user
}
async function getProfile() {
try {
setLoading(true)
const user = await getCurrentUser()
let { data, error, status } = await supabase
.from('profiles')
@@ -316,7 +382,8 @@ export default function Account({ session }) {
setAvatarUrl(data.avatar_url)
}
} catch (error) {
alert(error.message)
alert('Error loading user data!')
console.log(error)
} finally {
setLoading(false)
}
@@ -325,23 +392,21 @@ export default function Account({ session }) {
async function updateProfile({ username, website, avatar_url }) {
try {
setLoading(true)
const user = await getCurrentUser()
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
updated_at: new Date().toISOString(),
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) {
throw error
}
if (error) throw error
alert('Profile updated!')
} catch (error) {
alert(error.message)
alert('Error updating the data!')
console.log(error)
} finally {
setLoading(false)
}
@@ -354,7 +419,7 @@ export default function Account({ session }) {
<input id="email" type="text" value={session.user.email} disabled />
</div>
<div>
<label htmlFor="username">Name</label>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
@@ -395,63 +460,176 @@ export default function Account({ session }) {
}
```
</TabItem>
<TabItem value="ts">
Let's create a new component for that called `Account.tsx` within a `components` folder.
```tsx title="components/Account.tsx"
import { useState, useEffect } from 'react'
import {
useUser,
useSupabaseClient,
Session,
} from '@supabase/auth-helpers-react'
import { Database } from '../utils/database.types'
type Profiles = Database['public']['Tables']['profiles']['Row']
export default function Account({ session }: { session: Session }) {
const supabase = useSupabaseClient<Database>()
const user = useUser()
const [loading, setLoading] = useState(true)
const [username, setUsername] = useState<Profiles['username']>(null)
const [website, setWebsite] = useState<Profiles['website']>(null)
const [avatar_url, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
useEffect(() => {
getProfile()
}, [session])
async function getProfile() {
try {
setLoading(true)
if (!user) throw new Error('No user')
let { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (error && status !== 406) {
throw error
}
if (data) {
setUsername(data.username)
setWebsite(data.website)
setAvatarUrl(data.avatar_url)
}
} catch (error) {
alert('Error loading user data!')
console.log(error)
} finally {
setLoading(false)
}
}
async function updateProfile({
username,
website,
avatar_url,
}: {
username: Profiles['username']
website: Profiles['website']
avatar_url: Profiles['avatar_url']
}) {
try {
setLoading(true)
if (!user) throw new Error('No user')
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date().toISOString(),
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) throw error
alert('Profile updated!')
} catch (error) {
alert('Error updating the data!')
console.log(error)
} finally {
setLoading(false)
}
}
return (
<div className="form-widget">
<div>
<label htmlFor="email">Email</label>
<input id="email" type="text" value={session.user.email} disabled />
</div>
<div>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
value={username || ''}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="website">Website</label>
<input
id="website"
type="website"
value={website || ''}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
<div>
<button
className="button primary block"
onClick={() => updateProfile({ username, website, avatar_url })}
disabled={loading}
>
{loading ? 'Loading ...' : 'Update'}
</button>
</div>
<div>
<button
className="button block"
onClick={() => supabase.auth.signOut()}
>
Sign Out
</button>
</div>
</div>
)
}
```
</TabItem>
</Tabs>
### Launch!
Now that we have all the components in place, let's update `pages/index.js`:
```jsx title="pages/index.js"
import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabaseClient'
import Auth from '../components/Auth'
import type { NextPage } from 'next'
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
// highlight-next-line
import Account from '../components/Account'
export default function Home() {
const [isLoading, setIsLoading] = useState(true)
const [session, setSession] = useState(null)
useEffect(() => {
let mounted = true
async function getInitialSession() {
const {
data: { session },
} = await supabase.auth.getSession()
// only update the react state if the component is still mounted
if (mounted) {
if (session) {
setSession(session)
}
setIsLoading(false)
}
}
getInitialSession()
const { subscription } = supabase.auth.onAuthStateChange(
(_event, session) => {
setSession(session)
}
)
return () => {
mounted = false
subscription?.unsubscribe()
}
}, [])
const Home: NextPage = () => {
const session = useSession()
const supabase = useSupabaseClient()
return (
<div className="container" style={{ padding: '50px 0 100px 0' }}>
{!session ? (
<Auth />
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme="dark"
/>
) : (
<Account key={session.user.id} session={session} />
// highlight-next-line
<Account session={session} />
)}
</div>
)
}
export default Home
```
Once that's done, run this in a terminal window:
@@ -462,8 +640,6 @@ npm run dev
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
![Supabase User demo](/img/supabase-react-demo.png)
## Bonus: Profile photos
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like
@@ -471,14 +647,23 @@ photos and videos.
### Create an upload widget
Let's create an avatar for the user so that they can upload a profile photo.
We can start by creating a new component:
Let's create an avatar widget for the user so that they can upload a profile photo. We can start by creating a new component:
<Tabs
groupId="language"
defaultValue="js"
values={[
{label: 'JavaScript', value: 'js'},
{label: 'TypeScript', value: 'ts'},
]}>
<TabItem value="js">
```jsx title="components/Avatar.js"
import { useEffect, useState } from 'react'
import { supabase } from '../utils/supabaseClient'
import React, { useEffect, useState } from 'react'
import { useSupabaseClient } from '@supabase/auth-helpers-react'
export default function Avatar({ url, size, onUpload }) {
export default function Avatar({ uid, url, size, onUpload }) {
const supabase = useSupabaseClient()
const [avatarUrl, setAvatarUrl] = useState(null)
const [uploading, setUploading] = useState(false)
@@ -497,11 +682,11 @@ export default function Avatar({ url, size, onUpload }) {
const url = URL.createObjectURL(data)
setAvatarUrl(url)
} catch (error) {
console.log('Error downloading image: ', error.message)
console.log('Error downloading image: ', error)
}
}
async function uploadAvatar(event) {
const uploadAvatar = async (event) => {
try {
setUploading(true)
@@ -511,12 +696,12 @@ export default function Avatar({ url, size, onUpload }) {
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const fileName = `${uid}.${fileExt}`
const filePath = `${fileName}`
let { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file)
.upload(filePath, file, { upsert: true })
if (uploadError) {
throw uploadError
@@ -524,7 +709,8 @@ export default function Avatar({ url, size, onUpload }) {
onUpload(filePath)
} catch (error) {
alert(error.message)
alert('Error uploading avatar!')
console.log(error)
} finally {
setUploading(false)
}
@@ -566,6 +752,120 @@ export default function Avatar({ url, size, onUpload }) {
}
```
</TabItem>
<TabItem value="ts">
```tsx title="components/Avatar.tsx"
import React, { useEffect, useState } from 'react'
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { Database } from '../utils/database.types'
type Profiles = Database['public']['Tables']['profiles']['Row']
export default function Avatar({
uid,
url,
size,
onUpload,
}: {
uid: string
url: Profiles['avatar_url']
size: number
onUpload: (url: string) => void
}) {
const supabase = useSupabaseClient<Database>()
const [avatarUrl, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
const [uploading, setUploading] = useState(false)
useEffect(() => {
if (url) downloadImage(url)
}, [url])
async function downloadImage(path: string) {
try {
const { data, error } = await supabase.storage
.from('avatars')
.download(path)
if (error) {
throw error
}
const url = URL.createObjectURL(data)
setAvatarUrl(url)
} catch (error) {
console.log('Error downloading image: ', error)
}
}
const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (
event
) => {
try {
setUploading(true)
if (!event.target.files || event.target.files.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${uid}.${fileExt}`
const filePath = `${fileName}`
let { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file, { upsert: true })
if (uploadError) {
throw uploadError
}
onUpload(filePath)
} catch (error) {
alert('Error uploading avatar!')
console.log(error)
} finally {
setUploading(false)
}
}
return (
<div>
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar"
className="avatar image"
style={{ height: size, width: size }}
/>
) : (
<div
className="avatar no-image"
style={{ height: size, width: size }}
/>
)}
<div style={{ width: size }}>
<label className="button primary block" htmlFor="single">
{uploading ? 'Uploading ...' : 'Upload'}
</label>
<input
style={{
visibility: 'hidden',
position: 'absolute',
}}
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</div>
</div>
)
}
```
</TabItem>
</Tabs>
### Add the new widget
And then we can add the widget to the Account page:
@@ -580,6 +880,7 @@ return (
<div className="form-widget">
{/* Add to the body */}
<Avatar
uid={user.id}
url={avatar_url}
size={150}
onUpload={(url) => {
@@ -596,5 +897,8 @@ return (
At this stage you have a fully functional application!
- See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) and deploy it to Vercel.
- Explore the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs).
- Explore the [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui).
- Got a question? [Ask here](https://github.com/supabase/supabase/discussions).
- Sign in: [app.supabase.com](https://app.supabase.com)
+3 -3
View File
@@ -271,9 +271,9 @@ interface Props {
const Account: Component<Props> = ({ session }) => {
const [loading, setLoading] = createSignal(true)
const [username, setUsername] = (createSignal < string) | (null > null)
const [website, setWebsite] = (createSignal < string) | (null > null)
const [avatarUrl, setAvatarUrl] = (createSignal < string) | (null > null)
const [username, setUsername] = createSignal<string | null>(null)
const [website, setWebsite] = createSignal<string | null>(null)
const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
createEffect(() => {
getProfile()
+199 -298
View File
@@ -24,7 +24,7 @@ By the end of this guide you'll have an app which allows users to login and upda
### GitHub
Whenever you get stuck at any point, take a look at [this repo](https://github.com/zernonia/supabase-vue-3).
Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase/examples/tree/main/supabase-js-v2/user-management/vue3-user-management).
## Project set up
@@ -182,102 +182,141 @@ const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
```
And one optional step is to update the CSS file `src/assets/main.css` to make the app look nice.
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/react-user-management/src/index.css).
```javascript title="src/main.js"
import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.css'
createApp(App).mount('#app')
```
Optionally, update [src/style.css](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/vue3-user-management/src/style.css) to style the app.
### Set up a Login component
Let's set up a Vue component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
```html title="/src/components/Auth.vue"
<script setup>
import { ref } from 'vue'
import { supabase } from '../supabase'
const loading = ref(false)
const email = ref('')
const handleLogin = async () => {
try {
loading.value = true
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
loading.value = false
}
}
</script>
<template>
<form class="row flex-center flex" @submit.prevent="handleLogin">
<div class="col-6 form-widget">
<h1 class="header">Supabase + Vue 3</h1>
<p class="description">Sign in via magic link with your email below</p>
<div>
<input
class="inputField"
type="email"
placeholder="Your email"
v-model="email"
/>
<input class="inputField" type="email" placeholder="Your email" v-model="email" />
</div>
<div>
<input
type="submit"
class="button block"
:value="loading ? 'Loading' : 'Send magic link'"
:disabled="loading"
/>
<input type="submit" class="button block" :value="loading ? 'Loading' : 'Send magic link'"
:disabled="loading" />
</div>
</div>
</form>
</template>
<script>
import { ref } from 'vue'
import { supabase } from '../supabase'
export default {
setup() {
const loading = ref(false)
const email = ref('')
const handleLogin = async () => {
try {
loading.value = true
const { error } = await supabase.auth.signIn({ email: email.value })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
alert(error.error_description || error.message)
} finally {
loading.value = false
}
}
return {
loading,
email,
handleLogin,
}
},
}
</script>
```
### User store
To access the user information in other places, we use a reactive store. Create a new file called `store.js` and utilize Vue 3 `reactive` functionalities.
```javascript title="src/store.js"
import { reactive } from 'vue'
export const store = reactive({
user: {},
})
```
### Account page
After a user is signed in we can allow them to edit their profile details and manage their account.
Let's create a new component for that called `Profile.vue`.
Let's create a new component for that called `Account.vue`.
```html title="src/components/Account.vue"
<script setup>
import { supabase } from '../supabase'
import { onMounted, ref, toRefs } from 'vue'
const props = defineProps(['session'])
const { session } = toRefs(props)
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_url = ref('')
onMounted(() => {
getProfile()
})
async function getProfile() {
try {
loading.value = true
const { user } = session.value
let { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (error && status !== 406) throw error
if (data) {
username.value = data.username
website.value = data.website
avatar_url.value = data.avatar_url
}
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function updateProfile() {
try {
loading.value = true
const { user } = session.value
const updates = {
id: user.id,
username: username.value,
website: website.value,
avatar_url: avatar_url.value,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
</script>
```html title="src/components/Profile.vue"
<template>
<form class="form-widget" @submit.prevent="updateProfile">
<div>
<label for="email">Email</label>
<input id="email" type="text" :value="store.user.email" disabled />
<input id="email" type="text" :value="session.user.email" disabled />
</div>
<div>
<label for="username">Name</label>
@@ -289,12 +328,8 @@ Let's create a new component for that called `Profile.vue`.
</div>
<div>
<input
type="submit"
class="button primary block"
:value="loading ? 'Loading ...' : 'Update'"
:disabled="loading"
/>
<input type="submit" class="button primary block" :value="loading ? 'Loading ...' : 'Update'"
:disabled="loading" />
</div>
<div>
@@ -304,98 +339,6 @@ Let's create a new component for that called `Profile.vue`.
</div>
</form>
</template>
<script>
import { supabase } from '../supabase'
import { store } from '../store'
import { onMounted, ref } from 'vue'
export default {
setup() {
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_url = ref('')
async function getProfile() {
try {
loading.value = true
store.user = supabase.auth.user()
let { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', store.user.id)
.single()
if (error && status !== 406) throw error
if (data) {
username.value = data.username
website.value = data.website
avatar_url.value = data.avatar_url
}
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function updateProfile() {
try {
loading.value = true
store.user = supabase.auth.user()
const updates = {
id: store.user.id,
username: username.value,
website: website.value,
avatar_url: avatar_url.value,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
})
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
let { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
onMounted(() => {
getProfile()
})
return {
store,
loading,
username,
website,
avatar_url,
updateProfile,
signOut,
}
},
}
</script>
```
### Launch!
@@ -403,37 +346,31 @@ Let's create a new component for that called `Profile.vue`.
Now that we have all the components in place, let's update `App.vue`:
```html title="src/App.vue"
<script setup>
import { onMounted, ref } from 'vue'
import Account from './components/Account.vue'
import Auth from './components/Auth.vue'
import { supabase } from './supabase'
const session = ref()
onMounted(() => {
supabase.auth.getSession().then(({ data }) => {
session.value = data.session
})
supabase.auth.onAuthStateChange((_, _session) => {
session.value = _session
})
})
</script>
<template>
<div class="container" style="padding: 50px 0 100px 0">
<Profile v-if="store.user" />
<Account v-if="session" :session="session" />
<Auth v-else />
</div>
</template>
<script>
import { store } from './store'
import { supabase } from './supabase'
import Auth from './components/Auth.vue'
import Profile from './components/Profile.vue'
export default {
components: {
Auth,
Profile,
},
setup() {
store.user = supabase.auth.user()
supabase.auth.onAuthStateChange((_, session) => {
store.user = session.user
})
return {
store,
}
},
}
</script>
```
Once that's done, run this in a terminal window:
@@ -442,7 +379,7 @@ Once that's done, run this in a terminal window:
npm run dev
```
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
And then open the browser to [localhost:5173](http://localhost:5173) and you should see the completed app.
![Supabase Vue 3](/img/supabase-vue-3-demo.png)
@@ -455,132 +392,96 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
```html title="src/components/Avatar.vue"
<script setup>
import { ref, toRefs, watch } from 'vue'
import { supabase } from '../supabase'
const prop = defineProps(['path', 'size'])
const { path, size } = toRefs(prop)
const emit = defineEmits(['upload', 'update:path'])
const uploading = ref(false)
const src = ref('')
const files = ref()
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage
.from('avatars')
.download(path.value)
if (error) throw error
src.value = URL.createObjectURL(data)
} catch (error) {
console.error('Error downloading image: ', error.message)
}
}
const uploadAvatar = async (evt) => {
files.value = evt.target.files
try {
uploading.value = true
if (!files.value || files.value.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = files.value[0]
const fileExt = file.name.split('.').pop()
const filePath = `${Math.random()}.${fileExt}`
let { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file)
if (uploadError) throw uploadError
emit('update:path', filePath)
emit('upload')
} catch (error) {
alert(error.message)
} finally {
uploading.value = false
}
}
watch(path, () => {
if (path.value) downloadImage()
})
</script>
<template>
<div>
<img
v-if="src"
:src="src"
alt="Avatar"
class="avatar image"
:style="{ height: size, width: size }"
/>
<div
v-else
class="avatar no-image"
:style="{ height: size, width: size }"
/>
<img v-if="src" :src="src" alt="Avatar" class="avatar image"
:style="{ height: size + 'em', width: size + 'em' }" />
<div v-else class="avatar no-image" :style="{ height: size + 'em', width: size + 'em' }" />
<div :style="{ width: size }">
<div :style="{ width: size + 'em' }">
<label class="button primary block" for="single">
{{ uploading ? "Uploading ..." : "Upload" }}
</label>
<input
style="visibility: hidden; position: absolute"
type="file"
id="single"
accept="image/*"
@change="uploadAvatar"
:disabled="uploading"
/>
<input style="visibility: hidden; position: absolute" type="file" id="single" accept="image/*"
@change="uploadAvatar" :disabled="uploading" />
</div>
</div>
</template>
<script>
import { ref, toRefs, watch } from 'vue'
import { supabase } from '../supabase'
export default {
props: {
path: String,
},
emits: ['upload', 'update:path'],
setup(prop, { emit }) {
const { path } = toRefs(prop)
const size = ref('10em')
const uploading = ref(false)
const src = ref('')
const files = ref()
const downloadImage = async () => {
try {
const { data, error } = await supabase.storage
.from('avatars')
.download(path.value)
if (error) throw error
src.value = URL.createObjectURL(data)
} catch (error) {
console.error('Error downloading image: ', error.message)
}
}
const uploadAvatar = async (evt) => {
files.value = evt.target.files
try {
uploading.value = true
if (!files.value || files.value.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = files.value[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
let { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file)
if (uploadError) throw uploadError
emit('update:path', filePath)
emit('upload')
} catch (error) {
alert(error.message)
} finally {
uploading.value = false
}
}
watch(path, () => {
if (path.value) downloadImage()
})
return {
size,
uploading,
src,
files,
uploadAvatar,
}
},
}
</script>
```
### Add the new widget
And then we can add the widget to the Account page:
```html title="src/Profile.vue"
```html title="src/components/Account.vue"
<script>
// Import the new component
import Avatar from './Avatar.vue'
</script>
<template>
<form class="form-widget" @submit.prevent="updateProfile">
<!-- Add to body -->
<Avatar v-model:path="avatar_url" @upload="updateProfile" />
<Avatar v-model:path="avatar_url" @upload="updateProfile" size="10" />
<!-- Other form elements -->
</form>
</template>
<script>
// Import the new component
import Avatar from './Avatar.vue'
//
components: {
Avatar,
}
</script>
```
## Next steps
+1 -1
View File
@@ -14,7 +14,7 @@ const config = {
url: 'https://supabase.com',
baseUrl: baseUrl,
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
onBrokenMarkdownLinks: 'throw',
favicon: '/favicon.ico',
themes: ['docusaurus-theme-search-typesense'],
+47 -17
View File
@@ -4,7 +4,7 @@ const sidebars = {
type: 'category',
label: 'Overview',
collapsed: true,
items: ['about', 'architecture', 'guides/examples'],
items: ['about', 'guides/features', 'architecture', 'guides/examples'],
},
{
type: 'category',
@@ -80,6 +80,7 @@ const sidebars = {
'guides/auth/row-level-security',
'guides/auth/managing-user-data',
'guides/auth/auth-captcha',
'guides/auth/server-side-rendering',
],
},
{
@@ -215,22 +216,51 @@ const sidebars = {
label: 'Integrations',
collapsed: true,
items: [
'guides/integrations/appsmith',
'guides/integrations/auth0',
'guides/integrations/authsignal',
'guides/integrations/clerk',
'guides/integrations/dashibase',
'guides/integrations/directus',
'guides/integrations/draftbit',
'guides/integrations/pgmustard',
'guides/integrations/plasmic',
'guides/integrations/polyscale',
'guides/integrations/prisma',
'guides/integrations/sequin',
'guides/integrations/snaplet',
'guides/integrations/stytch',
'guides/integrations/supertokens',
'guides/integrations/vercel',
'guides/integrations/integrations',
{
type: 'category',
label: 'Auth',
collapsed: true,
items: [
'guides/integrations/auth0',
'guides/integrations/authsignal',
'guides/integrations/clerk',
'guides/integrations/keyri',
'guides/integrations/stytch',
'guides/integrations/supertokens',
],
},
{
type: 'category',
label: 'Caching / Offline-first',
collapsed: true,
items: ['guides/integrations/polyscale'],
},
{
type: 'category',
label: 'Developer Tools',
collapsed: true,
items: [
'guides/integrations/pgmustard',
'guides/integrations/prisma',
'guides/integrations/sequin',
'guides/integrations/snaplet',
'guides/integrations/vercel',
],
},
{
type: 'category',
label: 'Low-code',
collapsed: true,
items: [
'guides/integrations/appsmith',
'guides/integrations/dashibase',
'guides/integrations/dhiwise',
'guides/integrations/directus',
'guides/integrations/draftbit',
'guides/integrations/plasmic',
],
},
],
},
],
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

-1
View File
@@ -16,7 +16,6 @@ cd supabase
npm install
npm run dev
# visit website
http://localhost:3000
```
@@ -206,7 +206,7 @@ We'll focus on the end-user-centric meaning of performance here (query speed).
Postgres is capable of loading CSVs, but since we'll be using other search engines as well, let's convert to format that's much easier to use and process - [Newline Delimited JSON](https://dataprotocols.org/ndjson/).
After a few lines of code and one `[csv2ndjson.mjs` script](https://github.com/VADOSWARE/fts-benchmark/blob/main/src/util/csv2ndjson.mjs) later, we have a `movies.ndjson.json` full of JSON documents that we can easily ingest into any search engine (or other database for that matter!).
After a few lines of code and one [`csv2ndjson.mjs` script](https://github.com/VADOSWARE/fts-benchmark/blob/main/src/util/csv2ndjson.mjs) later, we have a `movies.ndjson.json` full of JSON documents that we can easily ingest into any search engine (or other database for that matter!).
### Inserting the data
@@ -31,11 +31,6 @@ const Benchmark = () => {
</p>
</div>
</div>
<div className="col-span-1 rounded-r-md px-12 pb-12 lg:col-span-2 lg:px-0 lg:pb-0">
<a href="https://supabasesubscribe.vercel.app/subscription/enterpriseBenchmarks">
<Button>Get notified</Button>
</a>
</div>
</div>
</div>
)
+119 -59
View File
@@ -270,7 +270,7 @@ module.exports = [
{
permanent: false,
source: '/docs/client/auth-session',
destination: '/docs/reference/javascript/auth-session',
destination: '/docs/reference/javascript/v1/auth-session',
},
{
permanent: false,
@@ -325,12 +325,12 @@ module.exports = [
{
permanent: false,
source: '/docs/client/removesubscription',
destination: '/docs/reference/javascript/removesubscription',
destination: '/docs/reference/javascript/v1/removesubscription',
},
{
permanent: false,
source: '/docs/client/getsubscriptions',
destination: '/docs/reference/javascript/getsubscriptions',
destination: '/docs/reference/javascript/v1/getsubscriptions',
},
{
permanent: false,
@@ -1359,60 +1359,120 @@ module.exports = [
destination: '/docs/guides/auth/auth-helpers/sveltekit',
},
// V2 redirects
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-update',
// destination: '/docs/reference/javascript/auth-updateuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-getuser',
// destination: '/docs/reference/javascript/auth-getuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-resetpasswordforemail',
// destination: '/docs/reference/javascript/auth-resetpasswordforemail',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-verifyotp',
// destination: '/docs/reference/javascript/auth-verifyotp',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-listusers',
// destination: '/docs/reference/javascript/auth-admin-listusers',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-createuser',
// destination: '/docs/reference/javascript/auth-admin-createuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-deleteuser',
// destination: '/docs/reference/javascript/auth-admin-deleteuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-generatelink',
// destination: '/docs/reference/javascript/auth-admin-generatelink',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-inviteuserbyemail',
// destination: '/docs/reference/javascript/auth-admin-inviteuserbyemail',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-getuserbyid',
// destination: '/docs/reference/javascript/auth-admin-getuserbyid',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-updateuserbyid',
// destination: '/docs/reference/javascript/auth-admin-updateuserbyid',
// },
// supabase-js v1 to v2 redirects
// v1: /auth-update
// v2: /auth-updateuser
{
permanent: true,
source: '/docs/reference/javascript/auth-update',
destination: '/docs/reference/javascript/v1/auth-update',
},
// v1: /auth-api-get-user
// v2: /auth-getuser
{
permanent: true,
source: '/docs/reference/javascript/auth-api-getuser',
destination: '/docs/reference/javascript/v1/auth-api-getuser',
},
// v1: /auth-api-resetpasswordforemail
// v2: /auth-resetpasswordforemail
{
permanent: true,
source: '/docs/reference/javascript/auth-api-resetpasswordforemail',
destination: '/docs/reference/javascript/v1/auth-api-resetpasswordforemail',
},
// v1: /auth-api-verifyotp
// v2: /auth-verifyotp
{
permanent: true,
source: '/docs/reference/javascript/auth-api-verifyotp',
destination: '/docs/reference/javascript/v1/auth-api-verifyotp',
},
// v1: /auth-api-listusers
// v2: /auth-admin-listusers
{
permanent: true,
source: '/docs/reference/javascript/auth-api-listusers',
destination: '/docs/reference/javascript/v1/auth-api-listusers',
},
// v1: /auth-api-createuser
// v2: /auth-admin-createuser
{
permanent: true,
source: '/docs/reference/javascript/auth-api-createuser',
destination: '/docs/reference/javascript/v1/auth-api-createuser',
},
// v1: /auth-api-deleteuser
// v2: /auth-admin-deleteuser
{
permanent: true,
source: '/docs/reference/javascript/auth-api-deleteuser',
destination: '/docs/reference/javascript/v1/auth-api-deleteuser',
},
// v1: /auth-api-generatelink
// v2: /auth-admin-generatelink
{
permanent: true,
source: '/docs/reference/javascript/auth-api-generatelink',
destination: '/docs/reference/javascript/v1/auth-api-generatelink',
},
// v1: /auth-api-inviteuserbyemail
// v2: /auth-admin-inviteuserbyemail
{
permanent: true,
source: '/docs/reference/javascript/auth-api-inviteuserbyemail',
destination: '/docs/reference/javascript/v1/auth-api-inviteuserbyemail',
},
// v1: /auth-api-getuserbyid
// v2: /auth-admin-getuserbyid
{
permanent: true,
source: '/docs/reference/javascript/auth-api-getuserbyid',
destination: '/docs/reference/javascript/v1/auth-api-getuserbyid',
},
// v1: /auth-api-updateuserbyid
// v2: /auth-admin-updateuserbyid
{
permanent: true,
source: '/docs/reference/javascript/auth-api-updateuserbyid',
destination: '/docs/reference/javascript/v1/auth-api-updateuserbyid',
},
// signIn method is now split into signInWithPassword ,signInWithPasswordless ,signInWithOAuth
// send traffic to v1 docs instead
{
permanent: true,
source: '/docs/reference/javascript/auth-signin',
destination: '/docs/reference/javascript/v1/auth-signin',
},
// v1: /auth-session
// v2: /auth-getsession
{
permanent: true,
source: '/docs/reference/javascript/auth-session',
destination: '/docs/reference/javascript/v1/auth-session',
},
// v1: /auth-api-sendmobileotp
// v2: /auth-signinwithotp
{
permanent: true,
source: '/docs/reference/javascript/auth-api-sendmobileotp',
destination: '/docs/reference/javascript/v1/auth-api-sendmobileotp',
},
// realtime methods been replaced with new names
{
permanent: true,
source: '/docs/reference/javascript/removesubscription',
destination: '/docs/reference/javascript/v1/removesubscription',
},
{
permanent: true,
source: '/docs/reference/javascript/removeallsubscriptions',
destination: '/docs/reference/javascript/v1/removeallsubscriptions',
},
{
permanent: true,
source: '/docs/reference/javascript/getsubscriptions',
destination: '/docs/reference/javascript/v1/getsubscriptions',
},
]
+1 -5
View File
@@ -49,11 +49,7 @@ const DPA = () => {
("DPA").
</p>
<p>
You can download the latest DPA document through our{' '}
<Link href="https://security.supabase.com/">security portal</Link>, or by
submitting your email here.
</p>
<p>You can download our latest DPA document by submitting your email here.</p>
{formSubmitted ? (
<p className="text-brand-900">
+1 -5
View File
@@ -18,11 +18,7 @@ const SOC2 = () => {
getting certified for SOC2 Type 2 and HIPAA next.
</p>
<p>
You can request for our latest SOC 2 document either through our{' '}
<Link href="https://security.supabase.com/">security portal</Link>, or by submitting
your request here.
</p>
<p>You can request for our latest SOC 2 document by submitting your request here.</p>
<SOC2RequestForm />
</div>
+2 -3
View File
@@ -52,7 +52,7 @@ export const Section = ({ children, icon, img }) => (
### SOC 2
Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 through our [Security Portal](https://security.supabase.com).
Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 [here](https://supabase.com/legal/soc2).
<div className="w-16">
<img src="/images/security/21972-312_SOC_NonCPA.png" />
@@ -126,8 +126,7 @@ In addition to internal security reviews, we use various tools to scan our code
<h3 className="text-3xl">Security Portal</h3>
Access Supabase’s security and compliance documents on our Security Portal, such as
penetration testing and audit reports.
Access more of Supabase’s security information on our Security Portal, such as penetration testing and audit reports.
<Link passHref href="https://security.supabase.com">
<Button type="default" size="medium" as="a" className="no-underline" target="_blank">
+38
View File
@@ -19,8 +19,10 @@
"mini-svg-data-uri": "^1.4.4"
},
"devDependencies": {
"@types/json-stringify-safe": "^5.0.0",
"eslint": "^7.32.0",
"eslint-config-custom": "*",
"json-stringify-safe": "^5.0.1",
"prettier": "^2.5.1",
"ts-jest": "^27.0.7",
"turbo": "1.4.7"
@@ -9345,6 +9347,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"node_modules/@types/json-stringify-safe": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/json-stringify-safe/-/json-stringify-safe-5.0.0.tgz",
"integrity": "sha512-UUA1sH0RSRROdInuDOA1yoRzbi5xVFD1RHCoOvNRPTNwR8zBkJ/84PZ6NhKVDtKp0FTeIccJCdQz1X2aJPr4uw==",
"dev": true
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -18169,6 +18177,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/html-to-image": {
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.10.8.tgz",
"integrity": "sha512-t+JyFJwKDCp4ZwBp4iC/wqw0meQDDc77Qs8OFl5P7RGlIP3LQMvwpD7VXxqQfC7/TfC+GKYlFP6WDYfXTmXHfw=="
},
"node_modules/html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@@ -22579,6 +22592,12 @@
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
@@ -36098,6 +36117,7 @@
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"json-logic-js": "^2.0.2",
"lodash": "^4.17.21",
@@ -43285,6 +43305,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"@types/json-stringify-safe": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/json-stringify-safe/-/json-stringify-safe-5.0.0.tgz",
"integrity": "sha512-UUA1sH0RSRROdInuDOA1yoRzbi5xVFD1RHCoOvNRPTNwR8zBkJ/84PZ6NhKVDtKp0FTeIccJCdQz1X2aJPr4uw==",
"dev": true
},
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -50190,6 +50216,11 @@
"integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==",
"dev": true
},
"html-to-image": {
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.10.8.tgz",
"integrity": "sha512-t+JyFJwKDCp4ZwBp4iC/wqw0meQDDc77Qs8OFl5P7RGlIP3LQMvwpD7VXxqQfC7/TfC+GKYlFP6WDYfXTmXHfw=="
},
"html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@@ -53550,6 +53581,12 @@
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
@@ -60621,6 +60658,7 @@
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"jest": "^27.4.0",
"jest-canvas-mock": "^2.3.1",
+4 -2
View File
@@ -28,11 +28,13 @@
"perf:meta": "ab -t 5 -c 20 -T application/json http://localhost:5555/tables"
},
"devDependencies": {
"@types/json-stringify-safe": "^5.0.0",
"eslint": "^7.32.0",
"eslint-config-custom": "*",
"json-stringify-safe": "^5.0.1",
"prettier": "^2.5.1",
"turbo": "1.4.7",
"ts-jest": "^27.0.7"
"ts-jest": "^27.0.7",
"turbo": "1.4.7"
},
"repository": {
"type": "git",
+24 -24
View File
@@ -46,18 +46,18 @@ module.exports = {
scale12: 'var(--colors-slate12)',
},
scaleA: {
scaleA1: 'var(--colors-slatea1)',
scaleA2: 'var(--colors-slatea2)',
scaleA3: 'var(--colors-slatea3)',
scaleA4: 'var(--colors-slatea4)',
scaleA5: 'var(--colors-slatea5)',
scaleA6: 'var(--colors-slatea6)',
scaleA7: 'var(--colors-slatea7)',
scaleA8: 'var(--colors-slatea8)',
scaleA9: 'var(--colors-slatea9)',
scaleA10: 'var(--colors-slatea10)',
scaleA11: 'var(--colors-slatea11)',
scaleA12: 'var(--colors-slatea12)',
scaleA1: 'var(--colors-slateA1)',
scaleA2: 'var(--colors-slateA2)',
scaleA3: 'var(--colors-slateA3)',
scaleA4: 'var(--colors-slateA4)',
scaleA5: 'var(--colors-slateA5)',
scaleA6: 'var(--colors-slateA6)',
scaleA7: 'var(--colors-slateA7)',
scaleA8: 'var(--colors-slateA8)',
scaleA9: 'var(--colors-slateA9)',
scaleA10: 'var(--colors-slateA10)',
scaleA11: 'var(--colors-slateA11)',
scaleA12: 'var(--colors-slateA12)',
},
scaleDark: {
scale1: 'var(--colors-gray1)',
@@ -74,17 +74,17 @@ module.exports = {
scale12: 'var(--colors-gray12)',
},
scaleADark: {
scaleA1: 'var(--colors-graya1)',
scaleA2: 'var(--colors-graya2)',
scaleA3: 'var(--colors-graya3)',
scaleA4: 'var(--colors-graya4)',
scaleA5: 'var(--colors-graya5)',
scaleA6: 'var(--colors-graya6)',
scaleA7: 'var(--colors-graya7)',
scaleA8: 'var(--colors-graya8)',
scaleA9: 'var(--colors-graya9)',
scaleA10: 'var(--colors-graya10)',
scaleA11: 'var(--colors-graya11)',
scaleA12: 'var(--colors-graya12)',
scaleA1: 'var(--colors-grayA1)',
scaleA2: 'var(--colors-grayA2)',
scaleA3: 'var(--colors-grayA3)',
scaleA4: 'var(--colors-grayA4)',
scaleA5: 'var(--colors-grayA5)',
scaleA6: 'var(--colors-grayA6)',
scaleA7: 'var(--colors-grayA7)',
scaleA8: 'var(--colors-grayA8)',
scaleA9: 'var(--colors-grayA9)',
scaleA10: 'var(--colors-grayA10)',
scaleA11: 'var(--colors-grayA11)',
scaleA12: 'var(--colors-grayA12)',
},
}
+1 -1
View File
@@ -53,7 +53,7 @@ function generateColorClasses() {
colors.map((x) => {
for (let index = 0; index < 12; index++) {
const step = index + 1
mappedColors[x][step * 100] = `var(--colors-${x.toLowerCase()}${step})`
mappedColors[x][step * 100] = `var(--colors-${x}${step})`
if (
fixedOptions.some(function (v) {
+8 -10
View File
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
import { TabsContext } from './TabsContext'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { useRouter } from 'next/router'
// @ts-ignore
// import TabsStyles from './Tabs.module.css'
@@ -48,11 +49,14 @@ function Tabs({
: ''
)
const router = useRouter()
const hash = router.asPath.split('#')[1]?.toUpperCase()
let __styles = styleHandler('tabs')
// activeId state can be overriden externally with `active`
// defaults to use activeTab
const active = activeId ? activeId : activeTab
// defaults to use a url #hash if we have one or activeTab if not
const active = activeId ? activeId : hash ? hash : activeTab
function onTabClick(id: string) {
const newTabSelected = id !== active
@@ -76,11 +80,7 @@ function Tabs({
}
return (
<TabsPrimitive.Root
defaultValue={defaultActiveId}
value={active}
className={__styles.base}
>
<TabsPrimitive.Root defaultValue={defaultActiveId} value={active} className={__styles.base}>
<TabsPrimitive.List className={listClasses.join(' ')}>
{addOnBefore}
{children.map((tab: any) => {
@@ -117,9 +117,7 @@ function Tabs({
{/* </Space> */}
{addOnAfter}
</TabsPrimitive.List>
<TabsContext.Provider value={{ activeId: active }}>
{children}
</TabsContext.Provider>
<TabsContext.Provider value={{ activeId: active }}>{children}</TabsContext.Provider>
</TabsPrimitive.Root>
)
}
+6 -7
View File
@@ -33,14 +33,13 @@ download.tsdoc.v1:
curl -sS https://supabase.github.io/storage-js/v1/spec.json > $(REPO_DIR)/enrichments/tsdoc_v1/storage.json
curl -sS https://supabase.github.io/functions-js/v1/spec.json > $(REPO_DIR)/enrichments/tsdoc_v1/functions.json
download.tsdoc.v2:
@echo ""
@echo "WARNING ............................................................"
@echo "WARNING: tsdoc v2 is not yet available for download"
@echo "WARNING: files should be updated manually"
@echo "WARNING ............................................................"
@echo ""
curl -sS https://supabase.github.io/supabase-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/supabase.json
curl -sS https://supabase.github.io/gotrue-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/gotrue.json
curl -sS https://supabase.github.io/postgrest-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/postgrest.json
curl -sS https://supabase.github.io/realtime-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/realtime.json
curl -sS https://supabase.github.io/storage-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/storage.json
curl -sS https://supabase.github.io/functions-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/functions.json
###############################################################################
# Transform docs into working files
+1 -2
View File
@@ -10,7 +10,6 @@ To get started, run `make init`. This installs all dependencies.
```bash
make # download latest specs and add enrichments
make.generate # transform the specs into docs
make # download and transform specs into docs
```
+172 -8
View File
@@ -1,7 +1,7 @@
clispec: '001'
info:
id: cli
version: 1.0.0
version: 1.8.5
title: Supabase CLI
language: sh
source: https://github.com/supabase/cli
@@ -11,8 +11,9 @@ info:
Supabase CLI provides you with tools to develop your application locally, and deploy your application to the Supabase platform.
options: |-
```sh
--debug output debug logs to stderr
-h, --help help for supabase
--debug output debug logs to stderr
-h, --help help for supabase
--workdir string path to a Supabase project directory
```
commands:
- id: supabase-stop
@@ -27,7 +28,8 @@ commands:
subcommands: []
options: |-
```sh
-h, --help help for stop
--backup Backs up the current database before stopping.
-h, --help help for stop
```
- id: supabase-status
title: supabase status
@@ -299,7 +301,7 @@ commands:
title: supabase gen types typescript
summary: Generate types for TypeScript
description: |
Generate types for TypeScript. Must specify either --local or --db-url
Generate types for TypeScript. Must specify one of --local, --linked, --project-id, or --db-url
tags: []
links: []
usage: |-
@@ -309,9 +311,12 @@ commands:
subcommands: []
options: |-
```sh
--db-url string Generate types from a database url.
-h, --help help for typescript
--local Generate types from the local dev database.
--db-url string Generate types from a database url.
-h, --help help for typescript
--linked Generate types from the linked project.
--local Generate types from the local dev database.
--project-id string Generate types from a project ID.
--schema stringArray Schemas to generate types for.
```
- id: supabase-functions
title: supabase functions
@@ -399,6 +404,7 @@ commands:
subcommands:
- supabase-db-branch
- supabase-db-diff
- supabase-db-lint
- supabase-db-push
- supabase-db-remote
- supabase-db-reset
@@ -478,6 +484,22 @@ commands:
-h, --help help for push
-p, --password string Password to your remote Postgres database.
```
- id: supabase-db-lint
title: supabase db lint
summary: Checks local database for typing error
tags: []
links: []
usage: |-
```sh
supabase db lint [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for lint
--level string Error level to emit. (default "warning")
-s, --schema strings List of schema to include. (default [public])
```
- id: supabase-db-diff
title: supabase db diff
summary: Diffs the local database for schema changes
@@ -567,3 +589,145 @@ commands:
```sh
-h, --help help for create
```
- id: supabase-completion
title: supabase completion
summary: Generate the autocompletion script for the specified shell
description: |
Generate the autocompletion script for supabase for the specified shell.
See each sub-command's help for details on how to use the generated script.
tags: []
links: []
subcommands:
- supabase-completion-bash
- supabase-completion-fish
- supabase-completion-powershell
- supabase-completion-zsh
options: |-
```sh
-h, --help help for completion
```
- id: supabase-completion-zsh
title: supabase completion zsh
summary: Generate the autocompletion script for zsh
description: |
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(supabase completion zsh); compdef _supabase supabase
To load completions for every new session, execute once:
#### Linux:
supabase completion zsh > "${fpath[1]}/_supabase"
#### macOS:
supabase completion zsh > $(brew --prefix)/share/zsh/site-functions/_supabase
You will need to start a new shell for this setup to take effect.
tags: []
links: []
usage: |-
```sh
supabase completion zsh [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for zsh
--no-descriptions disable completion descriptions
```
- id: supabase-completion-powershell
title: supabase completion powershell
summary: Generate the autocompletion script for powershell
description: |
Generate the autocompletion script for powershell.
To load completions in your current shell session:
supabase completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
tags: []
links: []
usage: |-
```sh
supabase completion powershell [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for powershell
--no-descriptions disable completion descriptions
```
- id: supabase-completion-fish
title: supabase completion fish
summary: Generate the autocompletion script for fish
description: |
Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
supabase completion fish | source
To load completions for every new session, execute once:
supabase completion fish > ~/.config/fish/completions/supabase.fish
You will need to start a new shell for this setup to take effect.
tags: []
links: []
usage: |-
```sh
supabase completion fish [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for fish
--no-descriptions disable completion descriptions
```
- id: supabase-completion-bash
title: supabase completion bash
summary: Generate the autocompletion script for bash
description: |
Generate the autocompletion script for the bash shell.
This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.
To load completions in your current shell session:
source <(supabase completion bash)
To load completions for every new session, execute once:
#### Linux:
supabase completion bash > /etc/bash_completion.d/supabase
#### macOS:
supabase completion bash > $(brew --prefix)/etc/bash_completion.d/supabase
You will need to start a new shell for this setup to take effect.
tags: []
links: []
usage: |-
```sh
supabase completion bash
```
subcommands: []
options: |-
```sh
-h, --help help for bash
--no-descriptions disable completion descriptions
```
+21
View File
@@ -3711,6 +3711,13 @@ pages:
not():
$ref: '@supabase/postgrest-js.PostgrestFilterBuilder.not'
notes: |
not() expects you to use the raw PostgREST syntax for the filter values.
```ts
.not('id', 'in', '(5,6,7)') // Use `()` for `in` filter
.not('arraycol', 'cs', '{"a","b"}') // Use `cs` for `contains()`, `{}` for array values
```
examples:
- name: With `select()`
description: |
@@ -3768,6 +3775,13 @@ pages:
or():
$ref: '@supabase/postgrest-js.PostgrestFilterBuilder.or'
notes: |
or() expects you to use the raw PostgREST syntax for the filter names and values.
```ts
.or('id.in.(5,6,7), arraycol.cs.{"a","b"}') // Use `()` for `in` filter, `{}` for array values and `cs` for `contains()`.
.or('id.in.(5,6,7), arraycol.cd.{"a","b"}') // Use `cd` for `containedBy()`
```
examples:
- name: With `select()`
description: |
@@ -3939,6 +3953,13 @@ pages:
filter():
$ref: '@supabase/postgrest-js.PostgrestFilterBuilder.filter'
notes: |
filter() expects you to use the raw PostgREST syntax for the filter values.
```ts
.filter('id', 'in', '(5,6,7)') // Use `()` for `in` filter
.filter('arraycol', 'cs', '{"a","b"}') // Use `cs` for `contains()`, `{}` for array values
```
examples:
- name: With `select()`
description: |
@@ -53,7 +53,7 @@ const Users = () => {
}
return (
<div className="overflow-scroll">
<div>
<div className="justify-between px-6 pt-6 pb-2 md:flex">
<div className="relative flex space-x-4">
<Input
@@ -108,8 +108,8 @@ const Users = () => {
{inviteEnabled && <InviteUserModal />}
</div>
</div>
<section className="mt-4 overflow-visible px-6">
<div className="section-block--body relative overflow-scroll rounded">
<section className="users-table-container mt-4 overflow-visible px-6">
<div className="section-block--body relative overflow-x-scroll rounded">
<div className="inline-block min-w-full align-middle">
<UsersList />
</div>
@@ -1,16 +0,0 @@
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Bug issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
]
@@ -0,0 +1,74 @@
import { IconArchive, IconDatabase, IconKey } from 'ui'
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Bug issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
]
export const USAGE_BASED_PRODUCTS = [
{
title: 'Database',
icon: <IconDatabase className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{
key: 'db_size',
attribute: 'total_db_size_bytes',
title: 'Database space',
units: 'bytes',
costPerUnit: 0.125,
},
{
key: 'db_egress',
attribute: 'total_egress_modified',
title: 'Database egress',
units: 'bytes',
costPerUnit: 0.09,
},
],
},
{
title: 'Auth',
icon: <IconKey className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{
key: 'monthly_active_users',
attribute: 'total_auth_billing_period_mau',
title: 'Monthly Active Users',
units: 'absolute',
costPerUnit: 0.00325,
},
],
},
{
title: 'Storage',
icon: <IconArchive className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{
key: 'storage_size',
attribute: 'total_storage_size_bytes',
title: 'Storage space',
units: 'bytes',
costPerUnit: 0.021,
},
{
key: 'storage_egress',
attribute: 'total_storage_egress',
title: 'Storage egress',
units: 'bytes',
costPerUnit: 0.09,
},
],
},
]
@@ -1,48 +0,0 @@
import { ChargeableProduct } from './PAYGUsage.types'
export const chargeableProducts: ChargeableProduct[] = [
{
title: 'Database',
iconUrl: '/img/database.svg',
features: [
{
title: 'Database Size',
attribute: 'total_db_size_bytes',
costPerUnit: 0.125,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
freeQuota: 8000000000, // 8GB
},
{
title: 'Database Egress',
attribute: 'total_egress_modified',
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
freeQuota: 50000000000, // 50GB
},
],
},
{
title: 'Storage',
iconUrl: '/img/archive.svg',
features: [
{
title: 'Storage Size',
attribute: 'total_storage_size_bytes',
costPerUnit: 0.021,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
freeQuota: 100000000000, // 100GB
},
{
title: 'Storage Egress',
attribute: 'total_storage_egress',
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
freeQuota: 200000000000, // 200GB
},
],
},
]
@@ -1,14 +1,11 @@
import React, { FC } from 'react'
import { FC } from 'react'
import dayjs from 'dayjs'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import { ChargeableProduct, PaygStats, ProductFeature } from './PAYGUsage.types'
import { deriveProductCost } from './PAYGUsage.utils'
import { chargeableProducts } from './PAYGUsage.constants'
import { DATE_FORMAT } from 'lib/constants'
import { USAGE_BASED_PRODUCTS } from '../Billing.constants'
interface Props {
paygStats: PaygStats
dateRange: {
period_start: { date: string; time_period: string }
period_end: { date: string; time_period: string }
@@ -16,16 +13,14 @@ interface Props {
}
}
const PAYGUsage: FC<Props> = ({ paygStats, dateRange }) => {
const PAYGUsage: FC<Props> = ({ dateRange }) => {
const startDate =
dateRange.period_start.date ?? dayjs().utc().startOf('month').format(DATE_FORMAT)
const endDate = dateRange.period_end.date ?? dayjs().utc().endOf('month').format(DATE_FORMAT)
return (
<div className="flex flex-col">
{chargeableProducts.map((product: ChargeableProduct) => {
const productCost = deriveProductCost(paygStats, product)
{USAGE_BASED_PRODUCTS.map((product) => {
return (
<div
key={product.title}
@@ -36,21 +31,15 @@ const PAYGUsage: FC<Props> = ({ paygStats, dateRange }) => {
<div className="flex items-center justify-between overflow-hidden rounded px-6 py-3">
<div className="flex items-center space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded bg-white">
<img width={'16'} src={product.iconUrl} />
{product.icon}
</div>
<h5 className="mb-0 font-medium">{product.title}</h5>
</div>
{product.title !== 'Authentication' && (
<div className="flex items-center space-x-1">
<p className="text-scale-1000">$</p>
<h4 className="text-lg m-0 font-medium">{productCost.toFixed(3)}</h4>
</div>
)}
</div>
</div>
<div className="flex flex-col">
{product.features.map((feature: ProductFeature) => (
{product.features.map((feature) => (
<div
key={feature.title}
className="border-panel-border-light dark:border-panel-border-dark relative border-t px-6 py-3"
@@ -1,18 +0,0 @@
export interface ChargeableProduct {
title: string
iconUrl: string
features: ProductFeature[]
}
export interface ProductFeature {
title: string
attribute: string
costPerUnit: number
unitQuantity: number
pricingModel: PricingModel
freeQuota?: number
}
export type PaygStats = { [key: string]: { sum: number; max: number } }
export type PricingModel = 'sum' | 'max'
@@ -1,39 +0,0 @@
import { get, maxBy, sum } from 'lodash'
import { ChargeableProduct, PaygStats, ProductFeature } from './PAYGUsage.types'
/**
*
* Derives the most appropriate cost
* depending on what kind of product feature we are displaying cost for.
*
* @param paygStats
* @param feature
*
* @returns number
*/
export const deriveFeatureCost = (paygStats: PaygStats | undefined, feature: ProductFeature) => {
let rawUsage = paygStats?.[feature.attribute]?.[feature.pricingModel]
/**
* Some features have a free quota
*
* Check there is included free quota,
* and remove free quota amount from the raw usage derived from paygStats
*
* if the number is negative, then Math.max should return 0
*
* todo: move this logic to backend @mildtomato
*/
if (rawUsage && feature.freeQuota) {
rawUsage = Math.max(0, rawUsage - feature.freeQuota)
}
const maximumValueOfTheMonth = rawUsage ?? 0
return (maximumValueOfTheMonth / feature.unitQuantity) * feature.costPerUnit
}
export const deriveProductCost = (paygStats: any, product: ChargeableProduct) => {
const costs = product.features.map((feature) => deriveFeatureCost(paygStats, feature))
return sum(costs)
}
@@ -6,17 +6,17 @@ interface Props {
amount: number | string
unitPrice: number | string
price: number | string
freeQuota?: number
note?: string
}
const CostBreakdownRow: FC<Props> = ({ item, amount, unitPrice, price, freeQuota }) => {
const CostBreakdownRow: FC<Props> = ({ item, amount, unitPrice, price, note }) => {
return (
<div className="relative flex items-center border-t border-panel-border-light px-6 py-3 dark:border-panel-border-dark">
<div className="flex w-[40%] items-center gap-3">
<span className="text-sm">{item}</span>
{freeQuota && (
{note && (
<Badge color="scale">
<span className="text-xs text-scale-900">{`${freeQuota / 1000000000}GB included`}</span>
<span className="text-xs text-scale-900">{note}</span>
</Badge>
)}
</div>
@@ -1,27 +1,23 @@
import dayjs from 'dayjs'
import { FC } from 'react'
import { sum } from 'lodash'
import { useRouter } from 'next/router'
import { Button, Loading } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useStore, useFlag } from 'hooks'
import { checkPermissions, useStore, useFlag, useProjectUsage } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
import { PaygStats, ProductFeature } from '../PAYGUsage/PAYGUsage.types'
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
import CostBreakdownRow from './CostBreakdownRow'
import { StripeSubscription } from './Subscription.types'
import NoPermission from 'components/ui/NoPermission'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
interface Props {
project: any
subscription: StripeSubscription
paygStats: PaygStats | undefined
loading?: boolean
showProjectName?: boolean
currentPeriodStart: number
@@ -31,7 +27,6 @@ interface Props {
const Subscription: FC<Props> = ({
project,
subscription,
paygStats,
loading = false,
showProjectName = false,
currentPeriodStart,
@@ -40,6 +35,7 @@ const Subscription: FC<Props> = ({
const { ui } = useStore()
const router = useRouter()
const { ref } = router.query
const enablePermissions = useFlag('enablePermissions')
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
@@ -48,6 +44,8 @@ const Subscription: FC<Props> = ({
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions')
: ui.selectedOrganization?.is_owner
const { usage, isLoading: loadingUsage } = useProjectUsage(ref as string)
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
const isEnterprise = subscription.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
@@ -56,15 +54,23 @@ const Subscription: FC<Props> = ({
const basePlanCost = subscription?.tier.unit_amount / 100
const deriveTotalCost = (): number => {
const totalAddOnCost = sum(addOns.map((addOn) => addOn.unit_amount / 100))
const totalUsageCost = isPayg
? Number(sum(chargeableProducts.map((product) => deriveProductCost(paygStats, product))))
: 0
const totalAddOnCost = addOns
.map((addOn) => addOn.unit_amount / 100)
.reduce((prev, current) => prev + current, 0)
const totalUsageCost =
usage === undefined
? 0
: Object.keys(usage)
.map((productKey) => {
return usage[productKey].cost
})
.reduce((prev, current) => prev + current, 0)
return basePlanCost + totalAddOnCost + totalUsageCost
}
return (
<Loading active={loading}>
<Loading active={loading || loadingUsage}>
<div className="mb-8 w-full overflow-hidden rounded border border-panel-border-light dark:border-panel-border-dark">
<div className="bg-panel-body-light dark:bg-panel-body-dark">
<div className="flex items-center justify-between px-6 pt-4">
@@ -164,8 +170,8 @@ const Subscription: FC<Props> = ({
<CostBreakdownRow
item="Base Plan"
amount={1}
unitPrice={basePlanCost}
price={basePlanCost}
unitPrice={basePlanCost.toFixed(2)}
price={basePlanCost.toFixed(2)}
/>
{addOns.map((addOn) => (
<CostBreakdownRow
@@ -177,25 +183,40 @@ const Subscription: FC<Props> = ({
/>
))}
{isPayg &&
chargeableProducts.map((product) =>
product.features.map((feature: ProductFeature) => {
const derivedCost = deriveFeatureCost(paygStats, feature)
const cost = derivedCost.toFixed(2)
USAGE_BASED_PRODUCTS.map((product) => {
return product.features.map((feature) => {
const amount = usage?.[feature.key]?.usage ?? 0
const limit = usage?.[feature.key]?.limit ?? 0
const cost = (usage?.[feature.key]?.cost ?? 0).toFixed(2)
return (
<CostBreakdownRow
key={feature.attribute}
key={feature.key}
item={feature.title}
freeQuota={feature.freeQuota}
amount={formatBytes(
paygStats?.[feature.attribute]?.[feature.pricingModel] ?? 0
)}
unitPrice={`${feature.costPerUnit}/GB`}
amount={
feature.units === 'bytes' ? formatBytes(amount) : amount.toLocaleString()
}
unitPrice={
feature.units === 'bytes'
? `${feature.costPerUnit}/GB`
: feature.costPerUnit
}
price={cost}
note={
feature.units === 'bytes'
? `${formatBytes(limit)} included`
: `${limit.toLocaleString()} included`
}
/>
)
})
)}
<div className="relative flex items-center border-t border-panel-border-light px-6 py-3 dark:border-panel-border-dark">
})}
<div
className={[
'relative flex items-center border-t px-6 py-3',
'border-panel-border-light dark:border-panel-border-dark',
].join(' ')}
>
<div className="w-[80%]">
<p className="text-sm text-scale-1100">
Estimated cost for {dayjs.unix(currentPeriodStart).utc().format('MMM D, YYYY')}{' '}
@@ -12,7 +12,6 @@ import UpdateSuccess from './UpdateSuccess'
import Invoices from './Invoices'
export * from './Subscription/Subscription.types'
export * from './PAYGUsage/PAYGUsage.constants'
export {
PAYGUsage,
Subscription,
@@ -16,7 +16,11 @@ const Landing = () => {
const { error } = await auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}?auth=true`,
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? process.env.NEXT_PUBLIC_VERCEL_URL
: process.env.NEXT_PUBLIC_SITE_URL
}?auth=true`,
},
})
if (error) throw error
@@ -78,7 +78,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</a>
</Link>
<Link href="https://supabase.com/docs/guides/database">
<a>
<a target="_blank" rel="noreferrer">
<Button type="default" icon={<IconExternalLink size={14} />}>
About Database
</Button>
@@ -120,7 +120,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</a>
</Link>
<Link href="https://supabase.com/docs/guides/auth">
<a>
<a target="_blank" rel="noreferrer">
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
@@ -154,7 +154,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</a>
</Link>
<Link href="https://supabase.com/docs/guides/storage">
<a>
<a target="_blank" rel="noreferrer">
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
@@ -189,7 +189,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</a>
</Link>
<Link href="https://supabase.com/docs/guides/functions">
<a>
<a target="_blank" rel="noreferrer">
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
@@ -217,7 +217,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</div>
<div className="flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/realtime">
<a>
<a target="_blank" rel="noreferrer">
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
@@ -258,7 +258,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</a>
</Link>
<Link href="https://supabase.com/docs/guides/api">
<a>
<a target="_blank" rel="noreferrer">
<Button className="translate-y-[1px]" type="default" icon={<IconExternalLink />}>
About APIs
</Button>
@@ -1,25 +0,0 @@
import { IconArchive, IconDatabase, IconKey } from 'ui'
export const usageBasedItems = [
{
title: 'Database',
icon: <IconDatabase className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{ key: 'db_size', title: 'Database space', units: 'bytes' },
{ key: 'db_egress', title: 'Database egress', units: 'bytes' },
],
},
{
title: 'Auth',
icon: <IconKey className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [{ key: 'monthly_active_users', title: 'Monthly Active Users', units: 'absolute' }],
},
{
title: 'Storage',
icon: <IconArchive className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{ key: 'storage_size', title: 'Storage space', units: 'bytes' },
{ key: 'storage_egress', title: 'Storage egress', units: 'bytes' },
],
},
]
@@ -6,8 +6,8 @@ import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import SparkBar from 'components/ui/SparkBar'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { usageBasedItems } from './ProjectUsageBars.constants'
import InformationBox from 'components/ui/InformationBox'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
interface Props {
projectRef?: string
@@ -48,7 +48,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
<Loading active={isLoading}>
{usage && (
<div>
{usageBasedItems.map((product) => {
{USAGE_BASED_PRODUCTS.map((product) => {
const isExceededUsage =
showUsageExceedMessage &&
product.features
@@ -1,6 +1,7 @@
export interface ResourceUsage {
usage: number
limit: number
cost: number
}
export interface UsageStats {
@@ -5,7 +5,7 @@ import { useProjectSubscription, useProjectUsage } from 'hooks'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import SparkBar from 'components/ui/SparkBar'
import { usageBasedItems } from './ProjectUsageBars.constants'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
interface ProjectUsageMinimalProps {
projectRef?: string
@@ -23,7 +23,7 @@ const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter
return <></>
}
const product = usageBasedItems.find((item) => item.title === filter)
const product = USAGE_BASED_PRODUCTS.find((item) => item.title === filter)
if (!product) return <></>
if (usageError || subscriptionError) {
@@ -212,7 +212,7 @@ const ColumnEditor: FC<Props> = ({
disabled={!isUndefined(columnFields?.foreignKey)}
description={
<Link href="https://supabase.com/docs/guides/database/tables#data-types">
<a>
<a target="_blank" rel="noreferrer">
<Button
as="span"
type="text"
@@ -319,7 +319,7 @@ const ColumnManagement: FC<Props> = ({
</Button>
)}
<Link href="https://supabase.com/docs/guides/database/tables#data-types">
<a>
<a target="_blank" rel="noreferrer">
<Button
type="text"
className="text-scale-1000 hover:text-scale-1200"
@@ -0,0 +1,64 @@
import { createClient } from '@supabase/supabase-js'
import { uuidv4 } from 'lib/helpers'
const SUPPORT_API_URL = process.env.NEXT_PUBLIC_SUPPORT_API_URL || ''
const SUPPORT_API_KEY = process.env.NEXT_PUBLIC_SUPPORT_ANON_KEY || ''
export const convertB64toBlob = (image: string) => {
const contentType = 'image/png'
const byteCharacters = atob(image.substr(`data:${contentType};base64,`.length))
const byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
const slice = byteCharacters.slice(offset, offset + 1024)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
const blob = new Blob(byteArrays, { type: contentType })
return blob
}
export const uploadAttachment = async (ref: string, image: string) => {
const supabaseClient = createClient(SUPPORT_API_URL, SUPPORT_API_KEY, {
auth: {
persistSession: false,
autoRefreshToken: false,
// @ts-ignore
multiTab: false,
detectSessionInUrl: false,
localStorage: {
getItem: (key: string) => undefined,
setItem: (key: string, value: string) => {},
removeItem: (key: string) => {},
},
},
})
const blob = convertB64toBlob(image)
const name = `${ref || 'no-project'}/${uuidv4()}.png`
const options = { cacheControl: '3600' }
const { data: file, error: uploadError } = await supabaseClient.storage
.from('feedback-attachments')
.upload(name, blob, options)
if (uploadError) {
console.error('Failed to upload:', uploadError)
return undefined
}
if (file) {
const { data } = await supabaseClient.storage
.from('feedback-attachments')
.createSignedUrls([file.path], 10 * 365 * 24 * 60 * 60)
return data?.[0].signedUrl
}
return undefined
}
@@ -1,11 +1,14 @@
import { useState, useEffect, useRef, FC } from 'react'
import Link from 'next/link'
import { useState, useEffect, useRef, FC } from 'react'
import { useRouter } from 'next/router'
import { Button, Input, Popover } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { toPng } from 'html-to-image'
import { Button, Input, Popover, IconCamera, IconX } from 'ui'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { convertB64toBlob, uploadAttachment } from './FeedbackDropdown.utils'
interface Props {
onClose: () => void
@@ -19,7 +22,10 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
const inputRef = useRef<any>(null)
const [feedback, setFeedback] = useState('')
const [screenshot, setScreenshot] = useState<string>()
const [isSending, setSending] = useState(false)
const [isSavingScreenshot, setIsSavingScreenshot] = useState(false)
useEffect(() => {
inputRef?.current?.focus()
@@ -29,15 +35,47 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
setFeedback(e.target.value)
}
function onCancel() {
onClose()
const captureScreenshot = () => {
setIsSavingScreenshot(true)
function filter(node: HTMLElement) {
if ((node?.children ?? []).length > 0) {
return node.children[0].id !== 'feedback-widget'
}
return true
}
toPng(document.body, { filter })
.then((dataUrl: any) => setScreenshot(dataUrl))
.catch((error: any) => {
ui.setNotification({
error,
category: 'error',
message: 'Failed to capture screenshot',
duration: 4000,
})
})
.finally(() => {
setIsSavingScreenshot(false)
})
}
const sendFeedback = async () => {
if (feedback.length) {
if (feedback.length === 0 && screenshot !== undefined) {
return ui.setNotification({
category: 'error',
message: 'Please include a message in your feedback.',
duration: 4000,
})
} else if (feedback.length > 0) {
setSending(true)
const attachmentUrl = screenshot
? await uploadAttachment(ref as string, screenshot)
: undefined
const formattedFeedback =
attachmentUrl !== undefined ? `${feedback}\n\nAttachments:\n${attachmentUrl}` : feedback
await post(`${API_URL}/feedback/send`, {
message: feedback,
message: formattedFeedback,
pathname: router.asPath,
category: 'Feedback',
projectRef: ref,
@@ -52,7 +90,7 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
}
return (
<div className="">
<div id="feedback-widget">
<Input.TextArea
className="w-80 p-3"
size="small"
@@ -64,12 +102,66 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
<Popover.Separator />
<div className="w-80 space-y-3 px-3 py-2 pb-4">
<div className="flex justify-between space-x-2">
<Button type="default" onClick={onCancel} className="hover:border-gray-500">
<Button type="default" onClick={onClose} className="hover:border-gray-500">
Cancel
</Button>
<Button disabled={isSending} loading={isSending} onClick={sendFeedback}>
Send feedback
</Button>
<div className="flex items-center space-x-2">
{screenshot !== undefined ? (
<div
style={{ backgroundImage: `url("${screenshot}")` }}
onClick={() => {
const blob = convertB64toBlob(screenshot)
const blobUrl = URL.createObjectURL(blob)
window.open(blobUrl, '_blank')
}}
className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat"
>
<div
className={[
'cursor-pointer rounded-full bg-red-900 h-3 w-3',
'flex items-center justify-center absolute -top-1 -right-1',
].join(' ')}
onClick={(event) => {
event.stopPropagation()
setScreenshot(undefined)
}}
>
<IconX size={8} strokeWidth={3} />
</div>
</div>
) : (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
as="span"
type="default"
disabled={isSavingScreenshot}
loading={isSavingScreenshot}
className="px-2 py-1.5"
icon={<IconCamera size={14} />}
onClick={() => captureScreenshot()}
/>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'bg-scale-100 rounded py-1 px-2 leading-none shadow', // background
'w-[130px] text-center border-scale-200 border', //border
].join(' ')}
>
<span className="text-scale-1200 text-xs">
Capture screenshot of current view
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
)}
<Button disabled={isSending} loading={isSending} onClick={sendFeedback}>
Send feedback
</Button>
</div>
</div>
<p className="text-xs text-scale-1000">
Have a technical issue? Contact{' '}
@@ -90,7 +90,7 @@ const NotificationActions: FC<Props> = ({
})}
{changelogLink && (
<Link href={changelogLink}>
<a>
<a target="_blank" rel="noreferrer">
<Button as="span" type="default" icon={<IconExternalLink size={12} strokeWidth={2} />}>
More info
</Button>
@@ -129,7 +129,7 @@ export const formatNotificationText = (
{upgrade.changelog_link && (
<div className="!ml-4">
<Link href={upgrade.changelog_link}>
<a>
<a target="_blank" rel="noreferrer">
<IconExternalLink
className="cursor-pointer text-scale-1000 hover:text-scale-1200 transition"
size={12}
@@ -16,6 +16,7 @@ import {
IconEdit2,
IconLoader,
IconChevronsUp,
IconList,
} from 'ui'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants.ts'
@@ -284,11 +285,18 @@ const FileExplorerHeader = ({
>
<Button
as="span"
icon={<IconColumns size={16} strokeWidth={2} />}
icon={
view === 'LIST' ? (
<IconList size={16} strokeWidth={2} />
) : (
<IconColumns size={16} strokeWidth={2} />
)
}
type="text"
disabled={breadcrumbs.length === 0}
onChange={onChangeView}
>
Views
View as
</Button>
</Dropdown>
<Dropdown
+12 -2
View File
@@ -1,6 +1,13 @@
import { useRef, useEffect, useState, FormEvent, KeyboardEvent, ReactNode } from 'react'
import { orderBy, filter, without } from 'lodash'
import { Popover, IconCheck, IconAlertCircle, IconSearch, IconPlus } from 'ui'
import {
Popover,
IconCheck,
IconAlertCircle,
IconSearch,
IconPlus,
IconChevronDown,
} from 'ui'
import { BadgeDisabled, BadgeSelected } from './Badges'
@@ -223,7 +230,7 @@ export default function MultiSelect({
>
<div
className={[
'flex w-full flex-wrap items-start items-center gap-1.5 p-1.5',
'flex w-full flex-wrap items-start gap-1.5 p-1.5 cursor-default',
`${selectedOptions.length === 0 ? 'h-9' : ''}`,
].join(' ')}
>
@@ -250,6 +257,9 @@ export default function MultiSelect({
)
}
})}
<div className="absolute inset-y-0 right-0 pl-3 pr-1 flex space-x-1 items-center cursor-pointer ">
<IconChevronDown className="text-scale-900" />
</div>
</div>
</Popover>
</div>
-1
View File
@@ -5,7 +5,6 @@ export * from './useFreeProjectLimitCheck'
export * from './usePermissions'
export * from './useProfile'
export * from './useProjectAuthConfig'
export * from './useProjectPaygStatistics'
export * from './useProjectSettings'
export * from './useProjectSubscription'
export * from './useSubscriptionStats'
@@ -1,82 +0,0 @@
import dayjs from 'dayjs'
import useSWR from 'swr'
import { get as _get, maxBy } from 'lodash'
import { get } from 'lib/common/fetch'
import { API_URL, DATE_FORMAT, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { chargeableProducts } from 'components/interfaces/Billing'
import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types'
/**
* Get project payg statistics
*
* @param ref project reference
* @param tierProdId tier product supabase_prod_id
*/
export function useProjectPaygStatistics(ref?: string, tierProdId?: string) {
const startDate = dayjs().utc().startOf('month').format(DATE_FORMAT)
const endDate = dayjs().utc().endOf('month').format(DATE_FORMAT)
const attributes =
'total_egress,total_db_size_bytes,total_db_egress_bytes,total_storage_size_bytes,total_storage_egress'
const url = `${API_URL}/projects/${ref}/daily-stats?attribute=${attributes}&startDate=${encodeURIComponent(
startDate
)}&endDate=${encodeURIComponent(endDate)}&interval='1d'`
/**
* data is a dictionary
* {
* data: array,
* totalAverage: number,
* total: number,
* maximum: number,
* format: string,
* }
*/
const { data, error } = useSWR<any>(
ref && tierProdId === PRICING_TIER_PRODUCT_IDS.PAYG ? url : null,
get
)
const anyError = data?.error || error
return {
paygStats: anyError ? undefined : getPaygStats(data?.data),
error: anyError,
isLoading: !anyError && !data,
isError: !!anyError,
}
}
function getPaygStats(data: any): PaygStats {
const paygStats: any = {}
if (!data) return paygStats
let dataModified: any = []
data.map((x: any) => {
dataModified.push({
...x,
total_egress_modified:
(x.total_egress == undefined ? 0 : x.total_egress) -
(x.total_storage_egress == undefined ? 0 : x.total_storage_egress),
})
})
chargeableProducts.forEach((product) => {
product.features.forEach((feature) => {
// calculate max
const max = _get(maxBy(dataModified, feature.attribute), feature.attribute) ?? 0
// calculate sum
let sum = 0
dataModified.map((x: any) => {
const featureAttribute = x[feature.attribute] == undefined ? 0 : x[feature.attribute]
sum = sum + featureAttribute
})
// set sum and max under attribute name
paygStats[feature.attribute] = {
sum,
max,
}
})
})
return paygStats
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default async function apiWrapper(
}
}
const func = withSentry(handler)
const func = withSentry(handler as any)
// @ts-ignore
return await func(req, res)
} catch (error) {
+2 -1
View File
@@ -32,13 +32,14 @@
"analytics-node": "^3.5.0",
"awesome-debounce-promise": "^2.1.0",
"clipboard": "^2.0.8",
"config": "*",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"json-logic-js": "^2.0.2",
"lodash": "^4.17.21",
+1
View File
@@ -10,6 +10,7 @@ import 'styles/contextMenu.scss'
import 'styles/react-data-grid-logs.scss'
import 'styles/date-picker.scss'
import 'styles/grid.scss'
import 'styles/users-table.scss'
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
+5 -1
View File
@@ -61,7 +61,11 @@ const UnauthorizedLanding = () => {
auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}?${params.toString()}`,
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? process.env.NEXT_PUBLIC_VERCEL_URL
: process.env.NEXT_PUBLIC_SITE_URL
}?${params.toString()}`,
},
})
}
+19 -4
View File
@@ -103,12 +103,27 @@ const OrgSettingsLayout = withAuth(
)
const OrganizationSettings: NextPageWithLayout = () => {
const router = useRouter()
const PageState: any = useContext(PageContext)
const { ui, app } = useStore()
const [selectedTab, setSelectedTab] = useState('GENERAL')
const { members, isError: isOrgDetailError } = useOrganizationDetail(
ui.selectedOrganization?.slug || ''
)
const slug = ui.selectedOrganization?.slug || ''
const { members, isError: isOrgDetailError } = useOrganizationDetail(slug || '')
const hash = router.asPath.split('#')[1]?.toUpperCase()
useEffect(() => {
if (hash) {
setSelectedTab(hash)
}
}, [hash])
function handleChangeTab(id: string) {
setSelectedTab(id)
router.push({
pathname: `/org/${slug}/settings`,
hash: id.toLowerCase(),
})
}
useEffect(() => {
if (!isOrgDetailError) {
@@ -128,7 +143,7 @@ const OrganizationSettings: NextPageWithLayout = () => {
<h1 className="text-3xl">{organization?.name || 'Organization'} settings</h1>
</section>
<nav className="">
<Tabs onChange={(id: any) => setSelectedTab(id)} type="underlined">
<Tabs onChange={(id: string) => handleChangeTab(id)} type="underlined">
<Tabs.Panel id="GENERAL" label="General" />
<Tabs.Panel id="TEAM" label="Team" />
<Tabs.Panel id="BILLING" label="Billing" />
@@ -4,14 +4,13 @@ import { observer } from 'mobx-react-lite'
import { Loading, IconArrowRight } from 'ui'
import { Project, NextPageWithLayout } from 'types'
import { useProjectPaygStatistics, useProjectSubscription, useStore } from 'hooks'
import { useProjectSubscription, useStore } from 'hooks'
import { STRIPE_PRODUCT_IDS, TIME_PERIODS_REPORTS, TIME_PERIODS_BILLING } from 'lib/constants'
import { SettingsLayout } from 'components/layouts'
import LoadingUI from 'components/ui/Loading'
import OveragesBanner from 'components/ui/OveragesBanner/OveragesBanner'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import { PAYGUsage, Subscription, Invoices } from 'components/interfaces/Billing'
import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types'
import ProjectUsage from 'components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars'
const ProjectBilling: NextPageWithLayout = () => {
@@ -39,9 +38,6 @@ interface SettingsProps {
const Settings: FC<SettingsProps> = ({ project }) => {
const { ui } = useStore()
const projectTier = ui.selectedProject?.subscription_tier
const [dateRange, setDateRange] = useState<any>()
const {
subscription,
@@ -49,10 +45,9 @@ const Settings: FC<SettingsProps> = ({ project }) => {
error,
} = useProjectSubscription(ui.selectedProject?.ref)
const { paygStats }: { paygStats: PaygStats | undefined } = useProjectPaygStatistics(
ui.selectedProject?.ref,
subscription?.tier?.supabase_prod_id
)
const [dateRange, setDateRange] = useState<any>()
const projectTier = ui.selectedProject?.subscription_tier
const isPayg = subscription?.tier?.prod_id === STRIPE_PRODUCT_IDS.PAYG
useEffect(() => {
if (error) {
@@ -74,7 +69,6 @@ const Settings: FC<SettingsProps> = ({ project }) => {
loading={loading}
project={project}
subscription={subscription}
paygStats={paygStats}
currentPeriodStart={subscription?.billing.current_period_start}
currentPeriodEnd={subscription?.billing.current_period_end}
/>
@@ -86,7 +80,7 @@ const Settings: FC<SettingsProps> = ({ project }) => {
</div>
</div>
</Loading>
) : subscription?.tier?.prod_id === STRIPE_PRODUCT_IDS.PAYG ? (
) : isPayg ? (
<div>
<div className="mb-4 flex items-center space-x-3">
<DateRangePicker
@@ -110,7 +104,7 @@ const Settings: FC<SettingsProps> = ({ project }) => {
</div>
)}
</div>
{paygStats && dateRange && <PAYGUsage paygStats={paygStats} dateRange={dateRange} />}
{dateRange && <PAYGUsage dateRange={dateRange} />}
</div>
) : (
<ProjectUsage projectRef={project?.ref} />
+13 -8
View File
@@ -32,12 +32,17 @@ span.th.tr.td.textarea {
}
.grid-monaco-editor,
.monaco-editor .suggest-widget .main {
max-height: none;
height: inherit;
width: 100%;
margin: inherit;
padding: inherit;
overflow: inherit;
background-color: inherit;
.monaco-editor .suggest-widget {
.main {
max-height: none;
height: inherit;
width: 100%;
margin: inherit;
padding: inherit;
overflow: inherit;
}
.monaco-list-row.focused .main {
background: var(--vscode-quickInput-background);
}
}
+39
View File
@@ -0,0 +1,39 @@
.users-table-container {
scrollbar-width: thin;
scrollbar-color: var(--colors-slate9) var(--colors-slate1);
.dark & {
scrollbar-color: var(--colors-slate10) var(--colors-slate3o);
}
::-webkit-scrollbar {
background: var(--colors-slate2);
height: 3px;
width: 0px;
border-radius: 0;
.dark & {
background: var(--colors-slate6);
}
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px var(--colors-slate6);
-webkit-border-radius: 5px;
border-radius: 5px;
.dark & {
-webkit-box-shadow: inset 0 0 6px var(--colors-slate8);
}
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 5px;
border-radius: 5px;
background: var(--colors-slate9);
.dark & {
-webkit-box-shadow: inset 0 0 6px var(--colors-slate12);
}
}
}