Merge branch 'master' into chore/check-is-platform-for-clippy

This commit is contained in:
Terry Sutton committed 2023-02-15 12:02:17 -03:30
commit 6d65cb685c
210 files changed
+14364 -1147

No files matched your search

-92
View File
@@ -1,92 +0,0 @@
name: Tests
# Controls when the workflow will run
on:
schedule:
- cron: '0 4/6 * * *'
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
autotests:
name: Run tests and generate Allure Report
strategy:
matrix:
node: ['14']
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./tests
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm ci
- uses: supabase/setup-cli@v1
- run: supabase start
- name: Run Test
run: npm run test:local
env:
SUPABASE_DB_PORT: 54322
SUPABASE_DB_PASS: postgres
SUPABASE_DB_HOST: localhost
SUPABASE_GOTRUE: http://localhost:54321
SUPABASE_URL: http://localhost:54321
SUPABASE_KEY_ANON: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
SUPABASE_KEY_ADMIN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU
- name: Stop infrastructure
if: always()
run: supabase stop
- name: Get Allure history
uses: actions/checkout@v2
if: always()
continue-on-error: true
with:
repository: supabase/test-reports
ref: gh-pages
path: gh-pages
- name: Allure Report action
uses: simple-elf/allure-report-action@master
if: always()
with:
github_repo: test-reports
allure_results: tests/allure-results
allure_history: allure-history
subfolder: supabase-integration
keep_reports: 50
- name: Deploy report to Github Pages
if: always()
uses: peaceiris/actions-gh-pages@v2
env:
EXTERNAL_REPOSITORY: supabase/test-reports
ACTIONS_DEPLOY_KEY: ${{ secrets.DEPLOY_TO_TEST_REPORTS_KEY }}
PUBLISH_BRANCH: gh-pages
PUBLISH_DIR: allure-history
- name: Post the link to the report
if: always()
uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
context: 'Test report'
state: 'success'
sha: ${{ github.event.pull_request.head.sha || github.sha }}
target_url: https://supabase.github.io/test-reports/supabase-integration/${{ github.run_number }}
+16 -48
View File
@@ -1,53 +1,21 @@
import providers from '../data/authProviders'
import ButtonCard from './ButtonCard'
import { IconPanel } from 'ui'
import Link from 'next/link'
export default function AuthProviders({ type }: { type: string }) {
const filterProviders = providers.filter((item) => item.authType === type)
export default function AuthProviders() {
return (
<div className="grid md:grid-cols-12 gap-4">
{providers.map((x) => (
<div key={x.name} className="col-span-6">
<ButtonCard to={x.href} title={x.name}>
<div className="px-6 py-4">
<div className="flex justify-between">
<p className="mt-0">{x.name}</p>
<p className="mt-0">
{x.official ? (
<span className={`badge badge--official`}>Official</span>
) : (
<span className={`badge badge--unofficial`}>Unofficial</span>
)}
</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<div
className="code-block"
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
fontSize: '0.7rem',
}}
>
<span>Platform:</span>
<span>{x.platform.toString()}</span>
</div>
<div
className="code-block"
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
fontSize: '0.7rem',
}}
>
<span>Self-Hosted:</span>
<span>{x.selfHosted.toString()}</span>
</div>
</div>
</div>
</ButtonCard>
</div>
))}
</div>
<>
<div className="grid grid-cols-12 gap-10 not-prose py-8">
{filterProviders.map((x) => (
<Link href={`${x.href}`} key={x.name} passHref>
<a className="col-span-6 lg:col-span-4 xl:col-span-3">
<IconPanel title={x.name} icon={x.logo} />
</a>
</Link>
))}
</div>
</>
)
}
+2 -2
View File
@@ -31,11 +31,11 @@ const questions = [
'How do I connect to my database?',
'How do I run migrations? ',
'How do I listen to changes in a table?',
'How do I setup authentication?',
'How do I set up authentication?',
]
function getEdgeFunctionUrl() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL.replace(/\/$/, '')
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
if (IS_PLATFORM) {
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
@@ -25,9 +25,10 @@ interface Props {
* expected tocList format (text, link,level).
*
* In tsx files, we can generate this tocList directly. For these files, we don't
* need to parse the <a> and generate anchors
* need to parse the <a> and generate anchors. Custom anchors are used in tsx files.
* (see: /pages/reference/cli/config.tsx)
*/
const Heading: React.FC<Props> = ({ tag, parseAnchors = false, customAnchor, children }) => {
const Heading: React.FC<Props> = ({ tag, customAnchor, children }) => {
const HeadingTag = `${tag}` as any
const anchor = customAnchor ? customAnchor : getAnchor(children)
const link = `#${anchor}`
@@ -43,7 +44,7 @@ const Heading: React.FC<Props> = ({ tag, parseAnchors = false, customAnchor, chi
return (
<HeadingTag id={anchor} ref={ref} className="group scroll-mt-24">
{parseAnchors ? removeAnchor(children) : children}
{removeAnchor(children)}
{anchor && (
<a href={link} className="ml-2 opacity-0 group-hover:opacity-100 transition">
#
+19 -20
View File
@@ -1,34 +1,33 @@
import { FC, useEffect, useState } from 'react'
import createConfigCatClient from 'configcat-js'
// import createConfigCatClient from 'configcat-js'
import FlagContext from './FlagContext'
const FlagProvider: FC = ({ children }) => {
const [store, setStore] = useState({})
const { Provider } = FlagContext
useEffect(() => {
getFlags()
}, [])
// useEffect(() => {
// getFlags()
// }, [])
const getFlags = async () => {
const setFlagValues = async () => {
const flagValues = await client.getAllValuesAsync()
const flagStore: any = {}
// const getFlags = async () => {
// const setFlagValues = async () => {
// const flagValues = await client.getAllValuesAsync()
// const flagStore: any = {}
flagValues.forEach((item: any) => {
flagStore[item.settingKey] = item.settingValue
})
setStore(flagStore)
}
// flagValues.forEach((item: any) => {
// flagStore[item.settingKey] = item.settingValue
// })
// setStore(flagStore)
// }
const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
configChanged: setFlagValues,
pollIntervalSeconds: 600,
})
// const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
// configChanged: setFlagValues,
// pollIntervalSeconds: 600,
// })
await setFlagValues()
}
// await setFlagValues()
// }
return <Provider value={store}>{children}</Provider>
}
@@ -400,15 +400,40 @@ export const database = {
},
{ name: 'http: RESTful Client', url: '/guides/database/extensions/http', items: [] },
{
name: 'PGRoonga: Multilingual Full Text Search',
name: 'PGAudit: Postgres Auditing',
url: '/guides/database/extensions/pgaudit',
items: [],
},
{
name: 'pgjwt: JSON Web Tokens',
url: '/guides/database/extensions/pgjwt',
items: [],
},
{
name: 'PGroonga: Multilingual Full Text Search',
url: '/guides/database/extensions/pgroonga',
items: [],
},
{
name: 'pgRouting: Geospatial Routing',
url: '/guides/database/extensions/pgrouting',
items: [],
},
{
name: 'pg_cron: Job Scheduling',
url: '/guides/database/extensions/pgcron',
items: [],
},
{
name: 'pg_graphql: GraphQL Support',
url: '/guides/database/extensions/pg_graphql',
items: [],
},
{
name: 'pg_hashids: Short UIDs',
url: '/guides/database/extensions/pg_hashids',
items: [],
},
{
name: 'pg_jsonschema: JSON Schema Validation',
url: '/guides/database/extensions/pg_jsonschema',
@@ -419,6 +444,16 @@ export const database = {
url: '/guides/database/extensions/pgnet',
items: [],
},
{
name: 'pg_plan_filter: Restrict Total Cost',
url: '/guides/database/extensions/pg_plan_filter',
items: [],
},
{
name: 'pg_stat_monitor: Extended Query Performance Monitoring',
url: '/guides/database/extensions/pg_stat_monitor',
items: [],
},
{
name: 'pgvector: Embeddings and vector similarity',
url: '/guides/database/extensions/pgvector',
@@ -439,7 +474,27 @@ export const database = {
url: '/guides/database/extensions/postgis',
items: [],
},
{
name: 'pg-safeupdate: Required Where Clauses',
url: '/guides/database/extensions/pg-safeupdate',
items: [],
},
{
name: 'pgsodium: Encryption Features',
url: '/guides/database/extensions/pgsodium',
items: [],
},
{ name: 'pgTAP: Unit Testing', url: '/guides/database/extensions/pgtap', items: [] },
{
name: 'plpgsql_check: PL/pgSQL Linter',
url: '/guides/database/extensions/plpgsql_check',
items: [],
},
{
name: 'timescaledb: Time-series data',
url: '/guides/database/extensions/timescaledb',
items: [],
},
{
name: 'uuid-ossp: Unique Identifiers',
url: '/guides/database/extensions/uuid-ossp',
@@ -450,6 +505,11 @@ export const database = {
url: '/guides/database/extensions/rum',
items: [],
},
{
name: 'wrappers: 3rd Party Integrations',
url: '/guides/database/extensions/wrappers',
items: [],
},
],
},
{
@@ -658,6 +718,11 @@ export const resources = {
url: '/guides/resources/migrating-to-supabase/heroku',
items: [],
},
{
name: 'Render',
url: '/guides/resources/migrating-to-supabase/render',
items: [],
},
],
},
{
@@ -720,6 +785,7 @@ export const migrate = {
{ name: 'Firestore Data', url: '/guides/migrations/firestore-data', items: [] },
{ name: 'Firebase Storage', url: '/guides/migrations/firebase-storage', items: [] },
{ name: 'Heroku', url: '/guides/migrations/heroku', items: [] },
{ name: 'Render', url: '/guides/migrations/render', items: [] },
],
}
+38 -17
View File
@@ -1,172 +1,193 @@
const authProviders = [
{
name: 'Apple',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/apple-icon',
href: '/guides/auth/social-login/auth-apple',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Azure',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/microsoft-icon',
href: '/guides/auth/social-login/auth-azure',
official: false,
supporter: 'TBD',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Bitbucket',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/bitbucket-icon',
href: '/guides/auth/social-login/auth-bitbucket',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Discord',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/discord-icon',
href: '/guides/auth/social-login/auth-discord',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Facebook',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/facebook-icon',
href: '/guides/auth/social-login/auth-facebook',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'GitHub',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/github-icon',
href: '/guides/auth/social-login/auth-github',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'GitLab',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/gitlab-icon',
href: '/guides/auth/social-login/auth-gitlab',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Google',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/google-icon',
href: '/guides/auth/social-login/auth-google',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Keycloak',
logo: '/docs/img/icons/keycloak-icon',
href: '/guides/auth/social-login/auth-keycloak',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'LinkedIn',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/linkedin-icon',
href: '/guides/auth/social-login/auth-linkedin',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'MessageBird',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/messagebird-icon',
href: '/guides/auth/phone-login/messagebird',
official: false,
supporter: 'MessageBird',
platform: true,
selfHosted: true,
authType: 'phone',
},
{
name: 'Notion',
// logo: '/img/libraries/notion-icon.svg',
logo: '/docs/img/icons/notion-icon',
href: '/guides/auth/social-login/auth-notion',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Slack',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/slack-icon',
href: '/guides/auth/social-login/auth-slack',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Spotify',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/spotify-icon',
href: '/guides/auth/social-login/auth-spotify',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twitter',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twitter-icon',
href: '/guides/auth/social-login/auth-twitter',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twitch',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twitch-icon',
href: '/guides/auth/social-login/auth-twitch',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Zoom',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/zoom-icon',
href: '/guides/auth/social-login/auth-zoom',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twilio',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twilio-icon',
href: '/guides/auth/phone-login/twilio',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'phone',
},
{
name: 'Vonage',
logo: '/docs/img/icons/vonage-icon',
href: '/guides/auth/phone-login/vonage',
official: false,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'phone',
},
]
+1
View File
@@ -53,6 +53,7 @@ const nextConfig = {
'github.com',
'user-images.githubusercontent.com',
'raw.githubusercontent.com',
'weweb-changelog.ghost.io',
],
},
experimental: {
@@ -257,7 +257,7 @@ Wrap an Action to check that the user has a valid session. If they're not logged
```ts title=src/routes/posts/+page.server.ts
import type { Actions } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { error, invalid } from '@sveltejs/kit'
import { error, fail } from '@sveltejs/kit'
export const actions: Actions = {
createPost: async (event) => {
@@ -276,7 +276,7 @@ export const actions: Actions = {
.insert({ content })
if (createPostError) {
return invalid(500, {
return fail(500, {
supabaseErrorMessage: createPostError.message,
})
}
@@ -293,7 +293,7 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
```ts
import type { Actions } from './$types'
import { invalid, redirect } from '@sveltejs/kit'
import { fail, redirect } from '@sveltejs/kit'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { AuthApiError } from '@supabase/supabase-js'
@@ -313,14 +313,14 @@ export const actions: Actions = {
if (error) {
if (error instanceof AuthApiError && error.status === 400) {
return invalid(400, {
return fail(400, {
error: 'Invalid credentials.',
values: {
email,
},
})
}
return invalid(500, {
return fail(500, {
error: 'Server error. Try again later.',
values: {
email,
+9 -1
View File
@@ -44,8 +44,16 @@ You can authenticate your users in several ways:
We provide a suite of Providers and login methods, as well as [Auth helpers](/docs/guides/auth/auth-helpers/).
#### Social Auth
<div className="container" style={{ padding: 0 }}>
<AuthProviders />
<AuthProviders type="social" />
</div>
#### Phone Auth
<div className="container" style={{ padding: 0 }}>
<AuthProviders type="phone" />
</div>
### Configure third-party providers
+6 -6
View File
@@ -209,12 +209,12 @@ curl '<SUPABASE_URL>/rest/v1/todos' \
</TabPanel>
</Tabs>
JS Reference: [select()](../reference/javascript/select),
[insert()](../reference/javascript/insert),
[update()](../reference/javascript/update),
[upsert()](../reference/javascript/upsert),
[delete()](../reference/javascript/delete),
[rpc()](../reference/javascript/rpc) (call Postgres functions).
JS Reference: [select()](/docs/reference/javascript/select),
[insert()](/docs/reference/javascript/insert),
[update()](/docs/reference/javascript/update),
[upsert()](/docs/reference/javascript/upsert),
[delete()](/docs/reference/javascript/delete),
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
### GraphQL API
@@ -63,7 +63,7 @@ drop extension if exists http;
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension, call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
</TabPanel>
</Tabs>
@@ -7,11 +7,10 @@ export const meta = {
}
`HypoPG` is PostgreSQL extension for creating hypothetical/virtual indexes. HypoPG allows users to rapidly create hypothetical/virtual indexes that have no resource cost (CPU, disk, memory) that are visible to the PostgreSQL query planner.
That allows users to quickly search for an index to improve a slow query without waiting for them to build.
## Usage
The motivation for HypoPG is to allow users to quickly search for an index to improve a slow query without consuming server resources or waiting for them to build.
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -40,14 +39,14 @@ drop extension if exists hypopg;
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
</TabPanel>
</Tabs>
### Speeding up a query
Given the following table and a simple query to select from the table by id:
Given the following table and a simple query to select from the table by `id`:
{/* prettier-ignore */}
```sql
@@ -0,0 +1,60 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pg-safeupdate',
title: 'pg-safeupdate: Required Where Clauses',
description: 'Requires a where clause for update and delete',
}
[pg-safeupdate](https://github.com/eradman/pg-safeupdate) is a PostgreSQL extension designed to prevent users from accidentally updating or deleting too many records in a single statement by requiring a "where" clause in all update and delete statements.
The `pg-safeupdate` extension is a useful tool for protecting data integrity and preventing accidental data loss. Without it, a user could accidentally execute an update or delete statement that affects all records in a table. With `pg-safeupdate`, users are required to be more deliberate in their update and delete statements, which reduces the risk of significant error.
## Enable the extension
`pg-safeupdate` can be enabled on a per connection basis:
{/* prettier-ignore */}
```sql
load 'safeupdate';
```
or for all connections:
{/* prettier-ignore */}
```sql
alter database some_db set session_preload_libraries = 'safeupdate';
```
## Usage
Let's take a look at an example to see how pg-safeupdate works. Suppose we have a table called `employees` with the following columns: `id`, `name`, `department`, and `date_of_birth`. We want to update the `date_of_birth` for a specific employee with the `id` of 12345. Here is what the query would look like if we forgot to add a "where" clause:
{/* prettier-ignore */}
```sql
load 'safeupdate';
update employees set date_of_birth = '1987-01-28';
```
This query updates the `date_of_birth` for all employees to 1987-01-28, which is not what we intended. With `pg-safeupdate` enabled, we receive an error message prompting us to add a "where" clause to the query:
{/* prettier-ignore */}
```sql
ERROR: UPDATE requires a WHERE clause
```
We would then update our query to include a "where" clause specifying the employee with the `id` of 12345:
{/* prettier-ignore */}
```sql
update employees set date_of_birth = '1987-01-28' where id = 12345;
```
## Resources
- Official [pg-safeupdate documentation](https://github.com/eradman/pg-safeupdate)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,116 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pg_graphql',
title: 'pg_graphql: GraphQL for PostgreSQL',
description: 'A GraphQL Interface for PostgreSQL',
}
[pg_graphql](https://supabase.github.io/pg_graphql/) is PostgreSQL extension for interacting with the database using [GraphQL](https://graphql.org) instead of SQL.
The extension reflects a GraphQL schema from the existing SQL schema and exposes it through a SQL function, `graphql.resolve(...)`. This enables any programming language that can connect to PostgreSQL to query the database via GraphQL with no additional servers, processes, or libraries.
The `pg_graphql` resolve method is designed to interop with [PostgREST](https://postgrest.org/en/stable/index.html), the tool that underpins the Supabase API, such that the `graphql.resolve` function can be called via RPC to safely and performantly expose the GraphQL API over HTTP/S.
For more information about how the SQL schema is reflected into a GraphQL schema, see the [pg_graphql API docs](https://supabase.github.io/pg_graphql/api/).
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pg_graphql" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "pg_graphql" extension
create extension pg_graphql;
-- Disable the "pg_graphql" extension
drop extension if exists pg_graphql;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
</TabPanel>
</Tabs>
## Usage
Given a table
{/* prettier-ignore */}
```sql
create table "Blog"(
id serial primary key,
name varchar(255) not null,
description varchar(255),
);
insert into "Blog"(name)
values ('My Blog');
```
The reflected GraphQL schema can be queries immediately as
{/* prettier-ignore */}
```sql
select
graphql.resolve($$
{
blogCollection(first: 1) {
edges {
node {
id,
name
}
}
}
}
$$);
```
returning the JSON
{/* prettier-ignore */}
```json
{
"data": {
"blogCollection": {
"edges": [
{
"node": {
"id": 1
"name": "My Blog"
}
}
]
}
}
}
```
Note that `pg_graphql` fully supports schema introspection so you can connect any GraphQL IDE or schema inspection tool to see the full set of fields and arguments available in the API.
## API
- [`graphql.resolve`](https://supabase.github.io/pg_graphql/sql_interface/): A SQL function for executing GraphQL queries.
## Resources
- Official [`pg_graphql` documentation](https://github.com/supabase/pg_graphql)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,81 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pg_hashids',
title: 'pg_hashids: Short UIDs',
description: 'Generate Short UIDs from Numbers',
}
[pg_hashids](https://github.com/iCyberon/pg_hashids) provides a secure way to generate short, unique, non-sequential ids from numbers. The hashes are intended to be small, easy-to-remember identifiers that can be used to obfuscate data (optionally) with a password, alphabet, and salt. For example, you may wish to hide data like user IDs, order numbers, or tracking codes in favor of `pg_hashid`'s unique identifers.
## Enable the Extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pg_hashids" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "pg_hashids" extension
create extension pg_hashids with schema extensions;
-- Disable the "pg_hashids" extension
drop extension if exists pg_hashids;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your `public` schema clean.
</TabPanel>
</Tabs>
## Usage
Suppose we have a table that stores order information, and we want to give customers a unique identifer without exposing the sequential `id` column. To do this, we can use `pg_hashid`'s `id_encode` function.
```sql
create table orders (
id serial primary key,
description text,
price_cents bigint
);
insert into orders (description, price_cents)
values ('a book', 9095);
select
id,
id_encode(id) as short_id,
description,
price_cents
from
orders;
id | short_id | description | price_cents
----+----------+-------------+-------------
1 | jR | a book | 9095
(1 row)
```
To reverse the `short_id` back into an `id`, there is an equivalent function named `id_decode`.
## Resources
- Official [pg_hashids documention](https://github.com/iCyberon/pg_hashids)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -6,11 +6,9 @@ export const meta = {
description: 'Validate json/jsonb with JSON Schema in PostgreSQL.',
}
[JSON Schema](https://json-schema.org) is a language for annotating and validating JSON documents. [`pg_jsonschema`](https://json-schema.org) is a PostgreSQL extension that adds the ability to validate PostgreSQL's built-in `json` and `jsonb` data types against a JSON Schema document.
[JSON Schema](https://json-schema.org) is a language for annotating and validating JSON documents. [`pg_jsonschema`](https://github.com/supabase/pg_jsonschema) is a PostgreSQL extension that adds the ability to validate PostgreSQL's built-in `json` and `jsonb` data types against JSON Schema documents.
## Usage
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -39,7 +37,7 @@ drop extension if exists pg_jsonschema;
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
</TabPanel>
</Tabs>
@@ -49,9 +47,9 @@ It's good practice to create the extension within a separate schema (like `exten
- [`json_matches_schema(schema json, instance json)`](https://github.com/supabase/pg_jsonschema#api): Checks if a `json` _instance_ conforms to a JSON Schema _schema_.
- [`jsonb_matches_schema(schema json, instance jsonb)`](https://github.com/supabase/pg_jsonschema#api): Checks if a `jsonb` _instance_ conforms to a JSON Schema _schema_.
### Validating a document
## Usage
Since `pg_jsonschema` exposes its utilities as functions, we can test it with a simple select statement:
Since `pg_jsonschema` exposes its utilities as functions, we can execute them with a select statement:
{/* prettier-ignore */}
```sql
@@ -62,7 +60,7 @@ select
);
```
`pg_jsonschema` is generally used in tandem with a [check constraint](https://www.postgresql.org/docs/current/ddl-constraints.html) as a way to constrain the contents of a json/b field to match a JSON Schema.
`pg_jsonschema` is generally used in tandem with a [check constraint](https://www.postgresql.org/docs/current/ddl-constraints.html) as a way to constrain the contents of a json/b column to match a JSON Schema.
{/* prettier-ignore */}
```sql
@@ -0,0 +1,97 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pg_plan_filter',
title: 'pg_plan_filter: Restrict Total Cost',
description: 'Block queries over a total cost limit',
}
[pg_plan_filter](https://github.com/pgexperts/pg_plan_filter) is PostgreSQL extension to block execution of statements where query planner's estimate of the total cost exceeds a threshold. This is intended to give database administrators a way to restrict the contribution an individual query has on database load.
## Enable the extension
`pg_plan_filter` can be enabled on a per connection basis:
{/* prettier-ignore */}
```sql
load 'plan_filter';
```
or for all connections:
{/* prettier-ignore */}
```sql
alter database some_db set session_preload_libraries = 'plan_filter';
```
## API
`plan_filter.statement_cost_limit`: restricts the maximum total cost for executed statements
`plan_filter.limit_select_only`: restricts to `select` statements
Note that `limit_select_only = true` is not the same as read-only because `select` statements may modfiy data e.g. through a function call.
## Example
To demonstrate total cost filtering, we'll compare how `plan_filter.statement_cost_limit` treats queries that are under and over its cost limit. First, we set up a table with some data:
{/* prettier-ignore */}
```sql
create table book(
id int primary key
);
-- CREATE TABLE
insert into book(id) select * from generate_series(1, 10000);
-- INSERT 0 10000
```
Next, we can review the explain plans for a single record select, and a whole table select.
{/* prettier-ignore */}
```sql
explain select * from book where id =1;
QUERY PLAN
---------------------------------------------------------------------------
Index Only Scan using book_pkey on book (cost=0.28..2.49 rows=1 width=4)
Index Cond: (id = 1)
(2 rows)
explain select * from book;
QUERY PLAN
---------------------------------------------------------
Seq Scan on book (cost=0.00..135.00 rows=10000 width=4)
(1 row)
```
Now we can choose a `statement_cost_filter` value between the total cost for the single select (2.49) and the whole table select (135.0) so one statement will succeed and one will fail.
{/* prettier-ignore */}
```sql
load 'plan_filter';
set plan_filter.statement_cost_limit = 50; -- between 2.49 and 135.0
select * from book where id = 1;
id
----
1
(1 row)
-- SUCCESS
```
{/* prettier-ignore */}
```sql
select * from book;
ERROR: plan cost limit exceeded
HINT: The plan for your query shows that it would probably have an excessive run time. This may be due to a logic error in the SQL, or it maybe just a very costly query. Rewrite your query or increase the configuration parameter "plan_filter.statement_cost_limit".
-- FAILURE
```
## Resources
- Official [pg_plan_filter documentation](https://github.com/pgexperts/pg_plan_filter)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,88 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pg_stat_monitor',
title: 'pg_stat_monitor: Query Performance Monitoring',
description: 'Query performance monitoring for PostgreSQL',
}
[pg_stat_monitor](https://github.com/percona/pg_stat_monitor) is query performance monitoring tool that provides query execution statistics in a SQL view named `pg_stat_monitor`. It tracks a superset of statistics available in [`pg_stat_statements`](https://www.postgresql.org/docs/current/pgstatstatements.html). Some of the most useful features are:
- **Time Interval Grouping**: configurable time buckets to track query usage over time
- **Capture Parameters**: optionally track parameters passed into queries instead of generic placeholders e.g. `$1`
- **Query Plan**: store query plans used for execution
For more information on query optimization, check out the [query performance guide](/docs/guides/platform/performance#examining-query-performance).
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pg_stat_monitor" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "pg_stat_monitor" extension
create extension pg_stat_monitor with schema extensions;
-- Disable the "pg_stat_monitor" extension
drop extension if exists pg_stat_monitor;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
</TabPanel>
</Tabs>
## Views
{/* prettier-ignore */}
```sql
select * from extensions.pg_stat_monitor;
```
The following table shows a subset of available columns:
| Column Type | Description |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| bucket bigint | Data collection unit. The number shows what bucket in a chain a record belongs to |
| bucket_start_time timestampz | The start time of the bucket |
| userid oid (references pg_authid.oid) | OID of user who executed the statement |
| dbid oid (references pg_database.oid) | OID of database in which the statement was executed |
| toplevel bool | True if the query was executed as a top-level statement (always true if pg_stat_statements.track is set to top) |
| client_ip inet | The IP address of a client that ran the query |
| queryid bigint | Hash code to identify identical normalized queries. |
| planid text | An internally generated ID of a query plan |
| query_plan text | The sequence of steps used to execute a query. This parameter is only available when pgsm_enable_query_plan is enabled |
| query text | Text of a representative statement |
| plans bigint | Number of times the statement was planned (if pg_stat_statements.track_planning is enabled, otherwise zero) |
| total_plan_time double precision | Total time spent planning the statement, in milliseconds (if pg_stat_statements.track_planning is enabled, otherwise zero) |
| min_plan_time double precision | Minimum time spent planning the statement, in milliseconds (if pg_stat_statements.track_planning is enabled, otherwise zero) |
A full list of statistics is available in the [pg_stat_monitor docs](https://docs.percona.com/pg-stat-monitor/reference.html#postgresql-15).
## Functions
- [`pg_stat_monitor_reset()`](https://docs.percona.com/pg-stat-monitor/functions.html): Resets the statistics tracked by the `pg_stat_monitor` view and deletes all previous data.
- [`pg_stat_monitor_version()`](https://docs.percona.com/pg-stat-monitor/functions.html): Displays the version of the `pg_stat_monitor` extension.
## Resources
- Official [`pg_stat_monitor` documentation](https://github.com/percona/pg_stat_monitor)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pg_stat_statements',
title: 'pg_stat_statements: SQL Planning and Execution Statistics',
title: 'pg_stat_statements: Query Performance Monitoring',
description:
'Track planning and execution statistics of all SQL statements executed on the database.',
}
@@ -22,9 +22,9 @@ export const meta = {
A full list of statistics is available in the [pg_stat_statements docs](https://www.postgresql.org/docs/current/pgstatstatements.html).
## Usage
For more information on query optimization, check out the [query performance guide](/docs/guides/platform/performance#examining-query-performance).
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -53,12 +53,12 @@ drop extension if exists pg_stat_statements;
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
</TabPanel>
</Tabs>
### Inspecting activity
## Inspecting activity
A common use for `pg_stat_statements` is to track down expensive or slow queries. The `pg_stat_statements` view contains a row for each executed query with statistics inlined. For example, you can leverage the statistics to identify frequently executed and slow queries against a given table.
@@ -74,15 +74,15 @@ select
from
pg_stat_statements
where
calls > 1000 -- at least 50 calls
and mean_exec_time > 2.0 -- averaging at least 2ms/call
and total_exec_time > 60000 -- at least one minute total server time spent
and query ilike '%user_in_organization%' -- filter to queries that touch the user_in_organization table
calls > 50 -- at least 50 calls
and mean_exec_time > 2.0 -- averaging at least 2ms/call
and total_exec_time > 60000 -- at least one minute total server time spent
and query ilike '%user_in_organization%' -- filter to queries that touch the user_in_organization table
order by
calls desc
```
From the results, an informed decision about which queries to optimize, index, or adjust can be made.
From the results, we can make an informed decision about which queries to optimize or index.
## Resources
@@ -0,0 +1,105 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pgaudit',
title: 'PGAudit: Postgres Auditing',
description: 'Session and object auditing via PostgreSQL standard logging',
}
[PGAudit](https://www.pgaudit.org) is a PostgreSQL extension for logging session and object auditing over the standard PostgreSQL logging utility.
PGAudit grants fine grain control over which statements and objects are emitted to logs.
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgaudit" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "pgaudit" extension
create extension pgaudit;
-- Disable the "pgaudit" extension
drop extension if exists pgaudit;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
</TabPanel>
</Tabs>
## Settings
The `pgaudit.log` setting controls which statements to log. Available values include:
- **read**: `SELECT` and `COPY` when the source is a relation or a query.
- **write**: `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and `COPY` when the destination is a relation.
- **function**: Function calls and `DO` blocks.
- **role**: Statements related to roles and privileges: `GRANT`, `REVOKE`, `CREATE/ALTER/DROP ROLE`.
- **ddl**: All `DDL` that is not included in the `ROLE` class.
- **misc**: Miscellaneous commands, e.g. `DISCARD`, `FETCH`, `CHECKPOINT`, `VACUUM`, `SET`.
- **misc_set**: Miscellaneous `SET` commands, e.g. `SET ROLE`.
- **all**: Include all of the above.
For a full list of available settings see [settings docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#settings). Be aware that the `all` setting will generate a very large volume of logs.
## Example
Given a pgaudit setting
{/* prettier-ignore */}
```sql
set pgaudit.log = 'read, ddl';
```
The following create table, insert and select statements
{/* prettier-ignore */}
```sql
create table account (
id int primary key,
name text,
description text
);
insert into account (id, name, description)
values (1, 'Foo Barsworth', 'Customer account');
select * from account;
```
Results in the log output
```text
AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account(
id int,
name text,
description text
);,<not logged>
AUDIT: SESSION,2,1,READ,SELECT,,,select * from account,,<not logged>
```
Note that the insert statement is not logged because we did not include the `write` option for `pgaudit.log`.
## Resources
- Official [`PGAudit` documentation](https://www.pgaudit.org)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,113 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pgjwt',
title: 'pgjwt: JSON Web Tokens',
description: 'Encode and decode JWTs in PostgreSQL',
}
The [pgjwt](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extension allows you to create and parse [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) within a PostgreSQL database. JWTs are commonly used for authentication and authorization in web applications and services.
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgjwt" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "pgjwt" extension
create extension pgjwt schema extensions;
-- Disable the "pgjwt" extension
drop extension if exists pgjwt;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
</TabPanel>
</Tabs>
## API
- [`sign(payload json, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Signs a JWT containng _payload_ with _secret_ using _algorithm_.
- [`verify(token text, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Decodes a JWT _token_ that was signed with _secret_ using _algorithm_.
Where:
- `payload` is an encrypted JWT represented as a string.
- `secret` is the private/secret passcode which is used to sign the JWT and verify its integrity.
- `algorithm` is the method used to sign the JWT using the secret.
- `token` is an encrypted JWT represented as a string.
## Usage
Once the extension is installed, you can use its functions to create and parse JWTs. Here's an example of how you can use the `sign` function to create a JWT:
{/* prettier-ignore */}
```sql
select
extensions.sign(
payload := '{"sub":"1234567890","name":"John Doe","iat":1516239022}',
secret := 'secret',
algorithm := 'HS256'
);
```
The pgjwt_encode function returns a string that represents the JWT, which can then be safely transmitted between parties.
{/* prettier-ignore */}
```
sign
---------------------------------
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX
VCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiw
ibmFtZSI6IkpvaG4gRG9lIiwiaWF0Ijo
xNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y9
22BhjWgQzWXcXNrz0ogtVhfEd2o
(1 row)
```
To parse a JWT and extract its claims, you can use the `verify` function. Here's an example:
{/* prettier-ignore */}
```sql
select
extensions.verify(
token := 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiRm9vIn0.Q8hKjuadCEhnCPuqIj9bfLhTh_9QSxshTRsA5Aq4IuM',
secret := 'secret',
algorithm := 'HS256'
);
```
Which returns the decoded contents and some associated metadata.
{/* prettier-ignore */}
```sql
header | payload | valid
-----------------------------+----------------+-------
{"alg":"HS256","typ":"JWT"} | {"name":"Foo"} | t
(1 row)
```
## Resources
- Official [`pgjwt` documentation](https://github.com/michelp/pgjwt)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -16,9 +16,8 @@ The pg_net API is in alpha. Functions signatures may change.
It differs from the `http` extension in that it is asynchronous by default. This makes it useful in blocking functions (like triggers).
## Usage
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -2,15 +2,14 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pgroonga',
title: 'PGRoonga: Multilingual Full Text Search',
title: 'PGroonga: Multilingual Full Text Search',
description: 'Full Text Search for multiple languages in PostgreSQL',
}
`PGroonga` is a PostgreSQL extension adding a full text search indexing method based on [Groonga](https://groonga.org). While native PostgreSQL supports full text indexing, it is limited to alphabet and digit based languages. `PGroonga` offers a wider range of character support making it viable for a superset of languages supported by PostgreSQL including Japanese, Chinese, etc.
## Usage
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -39,12 +38,10 @@ drop extension if exists pgroonga;
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
</TabPanel>
</Tabs>
### Creating a full text search index
## Creating a full text search index
Given a table with a `text` column:
@@ -0,0 +1,159 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pgrouting',
title: 'pgrouting: Geospatial Routing',
description: 'Extends PostGIS with Geospatial Routing',
}
[pgRouting](http://pgrouting.org) is PostgreSQL and [PostGIS](http://postgis.net) extension adding geospatial routing functionality.
The core functionality of `pgRouting` is a set of path finding algorithms including:
- All Pairs Shortest Path, Johnson’s Algorithm
- All Pairs Shortest Path, Floyd-Warshall Algorithm
- Shortest Path A\*
- Bi-directional Dijkstra Shortest Path
- Bi-directional A\* Shortest Path
- Shortest Path Dijkstra
- Driving Distance
- K-Shortest Path, Multiple Alternative Paths
- K-Dijkstra, One to Many Shortest Path
- Traveling Sales Person
- Turn Restriction Shortest Path (TRSP)
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgrouting" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "pgRouting" extension
create extension pgrouting cascade;
-- Disable the "pgRouting" extension
drop extension if exists pgRouting;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
</TabPanel>
</Tabs>
## Example
As an example, we'll solve the [traveling salesman probelem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using the pgRouting's `pgr_TSPeuclidean` function from some PostGIS coodinates.
A summary of the traveling salesman problem is, given a set of city coordinates, solve for a path that goes through each city and minimizes the total distance traveled.
First we populate a table with some X, Y coordinates
{/* prettier-ignore */}
```sql
create table wi29 (
id bigint,
x float,
y float,
geom geometry
);
insert into wi29 (id, x, y)
values
(1,20833.3333,17100.0000),
(2,20900.0000,17066.6667),
(3,21300.0000,13016.6667),
(4,21600.0000,14150.0000),
(5,21600.0000,14966.6667),
(6,21600.0000,16500.0000),
(7,22183.3333,13133.3333),
(8,22583.3333,14300.0000),
(9,22683.3333,12716.6667),
(10,23616.6667,15866.6667),
(11,23700.0000,15933.3333),
(12,23883.3333,14533.3333),
(13,24166.6667,13250.0000),
(14,25149.1667,12365.8333),
(15,26133.3333,14500.0000),
(16,26150.0000,10550.0000),
(17,26283.3333,12766.6667),
(18,26433.3333,13433.3333),
(19,26550.0000,13850.0000),
(20,26733.3333,11683.3333),
(21,27026.1111,13051.9444),
(22,27096.1111,13415.8333),
(23,27153.6111,13203.3333),
(24,27166.6667,9833.3333),
(25,27233.3333,10450.0000),
(26,27233.3333,11783.3333),
(27,27266.6667,10383.3333),
(28,27433.3333,12400.0000),
(29,27462.5000,12992.2222);
```
Next we use the `pgr_TSPeuclidean` function to find the best path.
{/* prettier-ignore */}
```sql
select
*
from
pgr_TSPeuclidean($$select * from wi29$$)
```
{/* prettier-ignore */}
```sql
seq | node | cost | agg_cost
-----+------+------------------+------------------
1 | 1 | 0 | 0
2 | 2 | 74.535614157127 | 74.535614157127
3 | 6 | 900.617093380362 | 975.152707537489
4 | 10 | 2113.77757765045 | 3088.93028518793
5 | 11 | 106.718669615254 | 3195.64895480319
6 | 12 | 1411.95293791574 | 4607.60189271893
7 | 13 | 1314.23824873744 | 5921.84014145637
8 | 14 | 1321.76283931305 | 7243.60298076942
9 | 17 | 1202.91366735569 | 8446.5166481251
10 | 18 | 683.333268292684 | 9129.84991641779
11 | 15 | 1108.05137466134 | 10237.9012910791
12 | 19 | 772.082339448903 | 11009.983630528
13 | 22 | 697.666150054665 | 11707.6497805827
14 | 23 | 220.141999627513 | 11927.7917802102
15 | 21 | 197.926372783442 | 12125.7181529937
16 | 29 | 440.456596290771 | 12566.1747492844
17 | 28 | 592.939989005405 | 13159.1147382898
18 | 26 | 648.288376333318 | 13807.4031146231
19 | 20 | 509.901951359278 | 14317.3050659824
20 | 25 | 1330.83095428717 | 15648.1360202696
21 | 27 | 74.535658878487 | 15722.6716791481
22 | 24 | 559.016994374947 | 16281.688673523
23 | 16 | 1243.87392358622 | 17525.5625971092
24 | 9 | 4088.0585364911 | 21613.6211336004
25 | 7 | 650.85409697993 | 22264.4752305803
26 | 3 | 891.004385199336 | 23155.4796157796
27 | 4 | 1172.36699411442 | 24327.846609894
28 | 8 | 994.708187806297 | 25322.5547977003
29 | 5 | 1188.01888359478 | 26510.5736812951
30 | 1 | 2266.91173136004 | 28777.4854126552
```
## Resources
- Official [`pgRouting` documentation](https://docs.pgrouting.org/latest/en/index.html)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,49 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'pgsodium',
title: 'pgsodium: Encryption Features',
description: 'Encryption library for PostgreSQL',
}
[pgsodium](https://github.com/michelp/pgsodium) is a PostgreSQL extension which provides SQL access to libsodium's high-level cryptographic algorithms. It also enables some Postgres specific features including:
- [Server Key Management](https://github.com/michelp/pgsodium#server-key-management)
- [Transparent Column Encryption](https://github.com/michelp/pgsodium#transparent-column-encryption)
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgsodium" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "pgsodium" extension
create extension pgsodium;
-- Disable the "pgsodium" extension
drop extension if exists pgsodium;
```
</TabPanel>
</Tabs>
## Resources
- Official [`pgsodium` documentation](https://github.com/michelp/pgsodium)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -15,9 +15,7 @@ Let's cover some basic concepts:
- Unit tests: allow you to test small parts of a system (like a database table!).
- TAP: stands for [Test Anything Protocol](http://testanything.org/). It is an framework which aims to simplify the error reporting during testing.
## Usage
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -45,12 +43,12 @@ drop extension if exists pgtap;
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
</TabPanel>
</Tabs>
### Testing tables
## Testing tables
```sql
begin;
@@ -68,7 +66,7 @@ API:
- [`has_index()`](https://pgtap.org/documentation.html#has_index): Checks for the existence of a named index associated with the named table.
- [`has_relation()`](https://pgtap.org/documentation.html#has_relation): Tests whether or not a relation exists in the database.
### Testing columns
## Testing columns
```sql
begin;
@@ -86,7 +84,7 @@ API:
- [`has_column()`](https://pgtap.org/documentation.html#has_column): Tests whether or not a column exists in a given table, view, materialized view or composite type.
- [`col_is_pk()`](https://pgtap.org/documentation.html#col_is_pk): Tests whether the specified column or columns in a table is/are the primary key for that table.
### Testing RLS Policies
## Testing RLS Policies
```sql
begin;
@@ -133,7 +131,7 @@ API:
- [`results_eq()`](https://pgtap.org/documentation.html#results_eq)
- [`results_ne()`](https://pgtap.org/documentation.html#results_ne)
### Testing Functions
## Testing Functions
```sql
begin;
@@ -0,0 +1,105 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'plpgsql_check',
title: 'plpgsql_check: PL/pgSQL Linter',
description: 'Lint PL/pgSQL code',
}
[plpgsql_check](https://github.com/okbob/plpgsql_check) is a PostgreSQL extension that lints plpgsql for syntax, semantic and other related issues. The tool helps developers to identify and correct errors before executing the code. plpgsql_check is most useful for developers who are working with large or complex SQL codebases, as it can help identify and resolve issues early in the development cycle.
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "plpgsql_check" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "plpgsql_check" extension
create extension plpgsql_check;
-- Disable the "plpgsql_check" extension
drop extension if exists plpgsql_check;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension you can call `drop extension`.
</TabPanel>
</Tabs>
## API
- [`plpgsql_check_function( ... )`](https://github.com/okbob/plpgsql_check#active-mode): Scans a function for errors.
`plpgsql_check_function` is highly customizable. For a complete list of available arguments see [the docs](https://github.com/okbob/plpgsql_check#arguments)
## Usage
To demonstrate `plpgsql_check` we can create a function with a known error. In this case we create a function `some_func`, that references a non-existent column `place.created_at`.
{/* prettier-ignore */}
```sql
create table place(
x float,
y float
);
create or replace function public.some_func()
returns void
language plpgsql
as $$
declare
rec record;
begin
for rec in select * from place
loop
-- Bug: There is no column `created_at` on table `place`
raise notice '%', rec.created_at;
end loop;
end;
$$;
```
Note that executing the function would not catch the invalid reference error because the `loop` does not execute if no rows are present in the table.
{/* prettier-ignore */}
```sql
select public.some_func();
some_func
───────────
(1 row)
```
Now we can use plpgsql_check's `plpgsql_check_function` function to identify the known error.
{/* prettier-ignore */}
```sql
select plpgsql_check_function('public.some_func()');
plpgsql_check_function
------------------------------------------------------------
error:42703:8:RAISE:record "rec" has no field "created_at"
Context: SQL expression "rec.created_at"
```
## Resources
- Official [`plpgsql_check` documentation](https://github.com/okbob/plpgsql_check)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -15,9 +15,8 @@ While Postgres natively runs SQL, it can also run other "procedural languages".
It can be used for database functions, triggers, queries and more.
## Usage
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -50,7 +49,7 @@ Procedural languages are automatically installed within `pg_catalog`, so you don
</TabPanel>
</Tabs>
### Create `plv8` functions
## Create `plv8` functions
Functions written in `plv8` are written just like any other PostgreSQL functions, only
with the `language` identifier set to `plv8`.
@@ -14,9 +14,7 @@ While you may be able to store simple lat/long geographic coordinates as a set o
The additional data types that PostGIS provides include [Point](https://postgis.net/docs/using_postgis_dbmanagement.html#Point), [Polygon](https://postgis.net/docs/using_postgis_dbmanagement.html#Polygon), [Linestring](https://postgis.net/docs/using_postgis_dbmanagement.html#LineString), and many more to represent different types of geographical data. In this guide, we will mainly focus on how to interact with `Point` type, which represents a single set of latitude and longitude. If you are interested in digging deeper, you can learn more about different data types on the [data management section of PostGIS docs](https://postgis.net/docs/using_postgis_dbmanagement.html).
## Usage
### Enable the extension
## Enable the extension
You can get started with PostGIS by enabling the PostGIS extension in your Supabase dashboard.
@@ -0,0 +1,99 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'timescaledb',
title: 'timescaledb: Time-Series data',
description: 'Scalable time-series data storage and analysis',
}
[timescaledb](https://docs.timescale.com/timescaledb/latest/) is a PostgreSQL extension designed for improved handling of time-series data. It provides a scalable, high-performance solution for storing and querying time-series data on top of a standard PostgreSQL database.
`timescaledb` uses a time-series-aware storage model and indexing techniques to improve performance of PostgreSQL in working with time-series data. The extension divides data into chunks based on time intervals, allowing it to scale efficiently, especially for large data sets. The data is then compressed, optimized for write-heavy workloads, and partitioned for parallel processing. `timescaledb` also includes a set of functions, operators, and indexes that work with time-series data to reduce query times, and make data easier to work with.
## Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "timescaledb" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "timescaledb" extension
create extension timescaledb;
-- Disable the "timescaledb" extension
drop extension if exists timescaledb;
```
</TabPanel>
</Tabs>
## Usage
To demonstrate how `timescaledb` works, let's consider a simple example where we have a table that stores temperature data from different sensors. We will create a table named "temperatures" and store data for two sensors.
First we create a hypertable, which is a virtual table that is partitioned into chunks based on time intervals. The hypertable acts as a proxy for the actual table and makes it easy to query and manage time-series data.
{/* prettier-ignore */}
```sql
create table temperatures (
time timestamp not null,
sensor_id int not null,
temperature double precision not null
);
select create_hypertable('temperatures', 'time');
```
Next, we can populate some values
{/* prettier-ignore */}
```sql
insert into temperatures (time, sensor_id, temperature)
values
('2023-02-14 09:00:00', 1, 23.5),
('2023-02-14 09:00:00', 2, 21.2),
('2023-02-14 09:05:00', 1, 24.5),
('2023-02-14 09:05:00', 2, 22.3),
('2023-02-14 09:10:00', 1, 25.1),
('2023-02-14 09:10:00', 2, 23.9),
('2023-02-14 09:15:00', 1, 24.9),
('2023-02-14 09:15:00', 2, 22.7),
('2023-02-14 09:20:00', 1, 24.7),
('2023-02-14 09:20:00', 2, 23.5);
```
And finally we can query the table using `timescaledb`'s `time_bucket` function to divide the time-series into intervals of the specified size (in this case, 1 hour) averaging the `temperature` reading within each group.
{/* prettier-ignore */}
```sql
select
time_bucket('1 hour', time) AS hour,
avg(temperature) AS average_temperature
from
temperatures
where
sensor_id = 1
and time > NOW() - interval '1 hour'
group by
hour;
```
## Resources
- Official [`timescaledb` documentation](https://docs.timescale.com/timescaledb/latest/)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -13,9 +13,7 @@ The `uuid-ossp` extension can be used to generate a `UUID`.
A `UUID` is a "Universally Unique Identifier" and it is, for practical purposes, unique.
This makes them particularly well suited as Primary Keys. It is occasionally referred to as a `GUID`, which stands for "Globally Unique Identifier".
## Usage
### Enable the extension
## Enable the extension
<Tabs
scrollable
@@ -46,7 +44,7 @@ drop extension if exists "uuid-ossp";
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension, call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
**Note**:
Currently `uuid-ossp` extension is enabled by default and cannot be disabled.
@@ -54,11 +52,11 @@ Currently `uuid-ossp` extension is enabled by default and cannot be disabled.
</TabPanel>
</Tabs>
### The `uuid` type
## The `uuid` type
Once the extension is enabled, you now have access to a `uuid` type.
### `uuid_generate_v1()`
## `uuid_generate_v1()`
Creates a UUID value based on the combination of computer’s MAC address, current timestamp, and a random value.
@@ -67,7 +65,7 @@ Creates a UUID value based on the combination of computer’s MAC address, curre
applications.
</Admonition>
### `uuid_generate_v4()`
## `uuid_generate_v4()`
Creates UUID values based solely on random numbers. You can also use Postgres's built-in [`gen_random_uuid()`](https://www.postgresql.org/docs/current/functions-uuid.html) function to generate a UUIDv4.
@@ -0,0 +1,75 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'wrappers',
title: 'wrappers: 3rd Party Integrations',
description: '3rd party integrations for PostgreSQL ',
}
[supabase/wrappers](https://supabase.github.io/wrappers/) is a PostgreSQL extension that provides integrations with external sources so you can interact with third-party data using SQL.
For example, the [Stripe wrapper](https://supabase.github.io/wrappers/stripe/) connects to [Stripe's API](https://stripe.com/docs/api) and exposes each endpoint as a SQL table.
{/* prettier-ignore */}
```sql
select
customer_id
currency
from
stripe.customers;
customer_id | currency
--------------------+-----------
cus_MJiBtCqOF1Bb3F | usd
(1 row)
```
## Usage
### Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "wrappers" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
{/* prettier-ignore */}
```sql
-- Enable the "wrappers" extension
create extension wrappers;
-- Disable the "wrappers" extension
drop extension if exists wrappers;
```
</TabPanel>
</Tabs>
Instructions for setting up connections to each 3rd party service via SQL are available at the links in the integrations section below.
A web interface for connecting to external data is coming to Supabase Studio in 2023.
## Integrations
- [Firebase](https://supabase.github.io/wrappers/firebase/)
- [Stripe](https://supabase.github.io/wrappers/stripe/)
- Airtable - Coming Soon
- BigQuery - Coming Soon
- Clickhouse - Coming Soon
## Resources
- Official [`supabase/wrappers` documentation](https://supabase.github.io/wrappers/)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -127,7 +127,7 @@ Edge Functions supports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, and `OPTIONS`.
- Deno Deploy limitations
- Deno does not support outgoing connections to ports `25`, `465`, and `587`.
- Cannot write to File System
- Cannot read or write to File System
- Edge Functions
- Local development - only one function at a time
- Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`).
@@ -6,37 +6,52 @@ export const meta = {
description: 'Understanding how database usage applies to your subscription.',
}
Database storage refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your project's [billing usage](https://app.supabase.com/project/_/settings/billing/usage) and is updated daily.
Database size refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your project's [billing usage](https://app.supabase.com/project/_/settings/billing/usage) and is updated daily.
Database size is the total size of used storage from your database, whereas disk size describes the size of the underlying available storage.
For an instantaneous live view of the DB storage usage, you can execute in Postgres:
For an instantaneous live view of the DB size, you can execute in Postgres:
```sql
select sum(pg_database_size(pg_database.datname)) / (1024 * 1024) as db_size_mb
from pg_database;
select
sum(pg_database_size (pg_database.datname)) / (1024 * 1024) as db_size_mb
from
pg_database;
```
This value is also reported in the [database settings page](https://app.supabase.com/project/_/settings/database).
## Database storage management
Supabase uses network-attached storage to balance performance with scalability. For Pro and Enterprise projects, database storage expands ~1.5x automatically (e.g., 8GB -> 12GB) when you reach 90% of the database storage quota. Automatic database storage expansion can only occur once every six hours. Pro projects can store up to 1024TB.
Supabase uses network-attached storage to balance performance with scalability.
For Pro and Enterprise projects, disk size expands ~1.5x automatically (e.g., 8GB -> 12GB) when you reach 90% of the disk size.
Disk size expansion can only occur once every six hours.
Pro projects can store up to 1024TB.
All projects enter read-only mode when you reach 95% of the database storage quota. In read-only mode, clients will encounter errors such as `cannot execute INSERT in a read-only transaction`. Regular operation (read-write mode) is automatically re-enabled once usage is below 95% of the database storage quota.
All projects enter read-only mode when you reach 95% of the disk size. In read-only mode, clients will encounter errors such as `cannot execute INSERT in a read-only transaction`.
Regular operation (read-write mode) is automatically re-enabled once usage is below 95% of the disk size.
If you need more than 1024TB of database storage or require multiple storage expansions in a short period of time, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
If you need more than 1024TB of disk size or require multiple storage expansions in a short period of time, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
### Increasing available database storage
### Increasing available disk size
1. [Upgrade to the Pro or Enterprise plan](https://app.supabase.com/project/_/settings/billing/subscription) to increase your storage quota.
1. [Upgrade to the Pro or Enterprise plan](https://app.supabase.com/project/_/settings/billing/subscription) to increase your quota and expand your disk size automatically.
2. Delete data from your project's database to lower its disk usage. If your database is already in read-only mode, run the following command to change the transaction mode to read-write for your session:
```sql
SET default_transaction_read_only = 'off';
SET
default_transaction_read_only = 'off';
```
This allows you to delete data from within the session.
### Preoccupied Space
When launching a new project, your database size will be roughly ~40-60mb.
The space is used up by preinstalled extensions, schema/data used by our services that are offered with each project and default Postgres data.
When installing additional extensions, even if you don't actively use them, additional database size is used.
## Vacuum operations
Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. Instead, they are internally marked as removed until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed.
+6
View File
@@ -135,6 +135,12 @@ export const migrationGuides = [
href: '/guides/resources/migrating-to-supabase/heroku',
description: 'Migrate your Heroku Postgres database to Supabase.',
},
{
title: 'Render',
icon: '/docs/img/icons/render-icon',
href: '/guides/resources/migrating-to-supabase/render',
description: 'Migrate your Render Postgres database to Supabase.',
},
]
export const postgres = [
@@ -0,0 +1,88 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Migrate from Render to Supabase',
description: 'Migrate your Render Postgres database to Supabase.',
}
Render is a popular Web Hosting service in the online services category that also has a managed Postgres service. Render has a great developer experience, allowing users to deploy straight from GitHub or GitLab. This is the core of their product and they do it really well. However, when it comes to Postgres databases, it may not be the best option.
Supabase is one of the best free alternative to Render Postgres. Supabase provide all the backend features developers need to build a product: a Postgres database, authentication, instant APIs, edge functions, realtime subscriptions, and storage. Postgres is the core of Supabase—for example, you can use row-level security and there are more than 40 Postgres extensions available.
This guide demonstrates how to migrate from Render to Supabase to get the most out of Postgres while gaining access to all the features you need to build a project.
## Retrieve your Render database credentials [#retrieve-render-credentials]
1. Log in to your [Render account](https://render.com) and select the project you want to migrate.
1. Click **Dashboard** in the menu and click in your **Postgres** database.
1. Scroll down in the **Info** tab.
1. Click on **PSQL Command** and edit it adding the content after `PSQL_COMMAND=`.
![Copying PSQL command from Render dashboard](/docs/img/guides/resources/migrating-to-supabase/render/render_dashboard.png)
Example:
```bash
%env PSQL_COMMAND=PGPASSWORD=RgaMDfTS_password_FTPa7 psql -h dpg-a_server_in.oregon-postgres.render.com -U my_db_pxl0_user my_db_pxl0
```
## Retrieve your Supabase Host [#retrieve-supabase-host]
1. If you're new to Supabase, [create a project](https://app.supabase.com).
1. Go to the [Database settings](https://app.supabase.com/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`).
1. Save your password or [reset it](https://app.supabase.com/project/_/settings/database) if you forgot it.
![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/render/supabase_dashboard.png)
## Migrate the database
The fastest way to migrate your database is with the Supabase migration tool on [Google Colab](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb). Alternatively, you can use the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tools, which are included in a full PostgreSQL installation.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="colab"
>
<TabPanel id="colab" label="Migrate using Colab">
1. Set the environment variables (`PSQL_COMMAND`, `SUPABASE_HOST`, `SUPABASE_PASSWORD`) in the Colab notebook.
1. Run the first two steps in [the notebook](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb) in order. The first sets the variables and the second installs PSQL and the migration script.
1. Run the third step to start the migration. This will take a few minutes.
</TabPanel>
<TabPanel id="cli" label="Migrate using CLI tools">
### Export your Render database to a file in console [#export-render-database]
Use `pg_dump` with your Render credentials to export your Render database to a file (e.g., `render_dump.sql`).
```bash
pg_dump --clean --if-exists --quote-all-identifiers \
-h $RENDER_HOST -U $RENDER_USER -d $RENDER_DATABASE \
--no-owner --no-privileges > render_dump.sql
```
### Import the database to your Supabase project [#import-database-to-supabase]
Use `psql` to import the Render database file to your Supabase project.
```bash
psql -h $SUPA_URL -U postgres --file render_dump.sql -p 6543 -d postgres
```
### Additional options
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
- To exclude a schema: `--exclude-schema=PATTERN`.
- To only migrate a single table: `--table=PATTERN`.
- To exclude a table: `--exclude-table=PATTERN`.
Run `pg_dump --help` for a full list of options.
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
-1
View File
@@ -13,7 +13,6 @@ Bo Lu
Bobbie Soedirgo
Chase Granberry
Chris Copplestone
Danny Ng
Dave Wilson
Div Arora
Egor Romanov
Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

@@ -0,0 +1,13 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17.4653" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="13.1709" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="13.1709" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.285156 7.2979C0.285156 3.72312 3.18309 0.825195 6.75786 0.825195H11.6109V3.65042H6.75786C4.74341 3.65042 3.11038 5.28345 3.11038 7.2979V16.259H0.285156V7.2979Z" fill="#46E7AA"/>
<rect x="8.87671" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="4.58228" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="0.288086" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="13.478" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="9.26123" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="5.04492" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+1
View File
@@ -7,6 +7,7 @@
.DocSearch-Container {
background: rgba(0, 0, 0, 0.3);
@apply fixed top-0 left-0 z-[200] flex h-screen w-screen flex-col p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh];
@apply cursor-auto #{!important};
}
.DocSearch-LoadingIndicator svg {
@@ -890,5 +890,6 @@ If you have any questions please reach out via [Twitter](https://twitter.com/dsh
- [Part 2: Flutter Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls)
- [Flutter Supabase Quick Starter Guide](https://supabase.com/docs/guides/with-flutter)
- [supabase-flutter v1.0 released](https://supabase.com/blog/supabase-flutter-sdk-v1-released)
- [How to build a real-time multiplayer game with Flutter Flame](https://supabase.com/blog/flutter-real-time-multiplayer-game)
- [Open source map based Flutter app with CI/CD and scalable statemanagement example](https://github.com/dshukertjr/spot)
- [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase)
@@ -160,4 +160,5 @@ If you have any feedbacks, please let us know in the issues of the [supabase-flu
- [supabase-flutter 1.0 developer preview](https://pub.dev/packages/supabase_flutter)
- [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app)
- [Flutter Tutorial - Part 2: Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls)
- [How to build a real-time multiplayer game with Flutter Flame](https://supabase.com/blog/flutter-real-time-multiplayer-game)
- [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase)
@@ -100,4 +100,5 @@ It required massive support from the community to bring the supabase-flutter to
- [v0 to v1 migration guide](https://supabase.com/docs/reference/dart/v0/upgrade-guide)
- [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app)
- [Flutter Tutorial - Part 2: Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls)
- [How to build a real-time multiplayer game with Flutter Flame](https://supabase.com/blog/flutter-real-time-multiplayer-game)
- [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase)
@@ -1357,6 +1357,7 @@ We can for example update this to a list of users that are online at the same ti
- [Complete set of code of this chat app](https://github.com/supabase-community/flutter-chat/tree/with_auth)
- [supabase-flutter docs](https://supabase.com/docs/reference/dart)
- [How to build a real-time multiplayer game with Flutter Flame](https://supabase.com/blog/flutter-real-time-multiplayer-game)
- [Flutter Supabase Quick Starter Guide](https://supabase.com/docs/guides/with-flutter)
- [supabase-flutter v1.0 released](https://supabase.com/blog/supabase-flutter-sdk-v1-released)
- [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase)
@@ -119,9 +119,9 @@ begin
select
id,
content,
(documents.embedding <=> query_embedding) as similarity
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where (documents.embedding <=> query_embedding) > similarity_threshold
where 1 - (documents.embedding <=> query_embedding) > similarity_threshold
order by documents.embedding <=> query_embedding
limit match_count;
end;
@@ -0,0 +1,840 @@
---
title: How to build a real-time multiplayer game with Flutter Flame
description: Build a real-time multiplayer game using Flutter, Flame, and Supabase realtime.
author: tyler_shukert
image: flutter-realtime-game/flutter-flame-realtime-multiplayer-game.png
thumb: flutter-realtime-game/flutter-flame-realtime-multiplayer-game.png
tags:
- flutter
- mobile
- realtime
date: '2023-02-14'
toc_depth: 3
---
Flutter is a UI library to build apps that run on any platform, but it can also build interactive games thanks to an open-source game engine built on top of Flutter called [Flame](https://flame-engine.org/). Flame takes care of things like collision detection or loading image sprites to bring game development to all the Flutter devs. We can take it a step further to introduce real-time communication features so that players can play against each other in real-time.
In this article, we will use Flutter, Flame, and Supabase's real-time features to build a real-time multiplayer shooting game. You can find the complete code of this tutorial [here](https://github.com/supabase/supabase/tree/master/examples/realtime/examples/flutter-multiplayer-shooting-game).
## Overview of the final game
<video width="99%" loop="" muted="" playsInline="" controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/flutter-realtime-game/playing-realtime-game.mp4"
type="video/mp4"
/>
</video>
The game is a simple shooting game. Each player has a UFO, and you can move it by dragging your finger across the screen. The UFO will emit bullets automatically in three directions, and the objective of the game is to hit the opponents with bullets before your UFO gets destroyed by the opponent’s bullets. The position and the health points are synced using a low-latency web socket connection provided by Supabase.
Before entering the main game, there is a lobby where you can wait for other players to show up. Once another player shows up, you can hit start, which will kick off the game on both ends.
We will first build the Flutter widgets used to build the basic UI, then build the Flame game and finally handle the network connection to share the data between connected clients.
## Build the App
### Step 1. Create the Flutter App
We will start out by creating the Flutter app. Open your terminal and create a new app named with the following command.
```bash
flutter create flame_realtime_shooting
```
Open the created app with your favorite IDE and let’s get started with coding!
### Step 2. Building the Flutter widgets
We will have a simple directory structure to build this app. Since we only have a few widgets, we will just add them inside `main.dart` file.
```
├── lib/
| ├── game/
│ │ ├── game.dart
│ │ ├── player.dart
│ │ └── bullet.dart
│ └── main.dart
```
### Create the Main Game Page
We will create minimal Flutter widgets for this app as most of the game logic will be handled in the Flame Game classes later. Our game will have a single page with two dialogs before the game starts and after the game ends. The page will simply contain the [GameWidget](https://docs.flame-engine.org/1.6.0/flame/game_widget.html) while displaying a nice background image. We will make it a StatefulWidget, because we will add methods to handle sending and receiving real-time data later on. Add the following to `main.dart` file.
```dart
import 'package:flame/game.dart';
import 'package:flame_realtime_shooting/game/game.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'UFO Shooting Game',
debugShowCheckedModeBanner: false,
home: GamePage(),
);
}
}
class GamePage extends StatefulWidget {
const GamePage({Key? key}) : super(key: key);
@override
State<GamePage> createState() => _GamePageState();
}
class _GamePageState extends State<GamePage> {
late final MyGame _game;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
Image.asset('assets/images/background.jpg', fit: BoxFit.cover),
GameWidget(game: _game),
],
),
);
}
@override
void initState() {
super.initState();
_initialize();
}
Future<void> _initialize() async {
_game = MyGame(
onGameStateUpdate: (position, health) async {
// TODO: handle gmae state update here
},
onGameOver: (playerWon) async {
// TODO: handle when the game is over here
},
);
// await for a frame so that the widget mounts
await Future.delayed(Duration.zero);
if (mounted) {
_openLobbyDialog();
}
}
void _openLobbyDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return _LobbyDialog(
onGameStarted: (gameId) async {
// handle game start here
},
);
});
}
}
```
You will see some errors, because we are importing some files that we haven’t created yet, but don’t worry, because we will get to it soon.
### Create the Lobby Dialog
![Flutter Flame shooting game lobby dialog](/images/blog/flutter-realtime-game/lobby.png)
Lobby dialog class on the surface is a simple Alert dialog, but will hold its own states like the list of players that are waiting at the lobby. We will also add some classes to handle the presence data on the lobby later on, but for now we will just have a simple AlertDialog. Add the following code at the end of main.dart file.
```dart
class _LobbyDialog extends StatefulWidget {
const _LobbyDialog({
required this.onGameStarted,
});
final void Function(String gameId) onGameStarted;
@override
State<_LobbyDialog> createState() => _LobbyDialogState();
}
class _LobbyDialogState extends State<_LobbyDialog> {
final List<String> _userids = [];
bool _loading = false;
/// TODO: assign unique identifier for the user
final myUserId = '';
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Lobby'),
content: _loading
? const SizedBox(
height: 100,
child: Center(child: CircularProgressIndicator()),
)
: Text('${_userids.length} users waiting'),
actions: [
TextButton(
onPressed: _userids.length < 2
? null
: () async {
setState(() {
_loading = true;
});
// TODO: notify the other player the start of the game
},
child: const Text('start'),
),
],
);
}
}
```
### Step 3. Building the Flame components
### Creating the FlameGame
Now the fun part starts! We will start out by creating our game class. We create a `MyGame` class that extends the [FlameGame](https://docs.flame-engine.org/1.6.0/flame/game.html) class. `FlameGame` takes care of collision detection and pan-detection, and it will also be the parent of all the components that we will add to the game. The game contains 2 components, `Player` and `Bullet`. `MyGame ` is a class that wraps around all components of the game and can control the child components.
![Structure of the shooting game](/images/blog/flutter-realtime-game/game-structure.png)
Let’s add flame to our app by adding the following to our `pubspec.yaml` file to install flame.
```yaml
flame: ^1.6.0
```
We can then create `MyGame` class. Add the following code in `lib/game.dart` file.
```dart
import 'dart:async';
import 'package:flame/game.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/image_composition.dart' as flame_image;
import 'package:flame_realtime_shooting/game/bullet.dart';
import 'package:flame_realtime_shooting/game/player.dart';
import 'package:flutter/material.dart';
class MyGame extends FlameGame with PanDetector, HasCollisionDetection {
MyGame({
required this.onGameOver,
required this.onGameStateUpdate,
});
static const _initialHealthPoints = 100;
/// Callback to notify the parent when the game ends.
final void Function(bool didWin) onGameOver;
/// Callback for when the game state updates.
final void Function(
Vector2 position,
int health,
) onGameStateUpdate;
/// `Player` instance of the player
late Player _player;
/// `Player` instance of the opponent
late Player _opponent;
bool isGameOver = true;
int _playerHealthPoint = _initialHealthPoints;
late final flame_image.Image _playerBulletImage;
late final flame_image.Image _opponentBulletImage;
@override
Color backgroundColor() {
return Colors.transparent;
}
@override
Future<void>? onLoad() async {
final playerImage = await images.load('player.png');
_player = Player(isMe: true);
final spriteSize = Vector2.all(Player.radius * 2);
_player.add(SpriteComponent(sprite: Sprite(playerImage), size: spriteSize));
add(_player);
final opponentImage = await images.load('opponent.png');
_opponent = Player(isMe: false);
_opponent.add(SpriteComponent.fromImage(opponentImage, size: spriteSize));
add(_opponent);
_playerBulletImage = await images.load('player-bullet.png');
_opponentBulletImage = await images.load('opponent-bullet.png');
await super.onLoad();
}
@override
void onPanUpdate(DragUpdateInfo info) {
_player.move(info.delta.global);
final mirroredPosition = _player.getMirroredPercentPosition();
onGameStateUpdate(mirroredPosition, _playerHealthPoint);
super.onPanUpdate(info);
}
@override
void update(double dt) {
super.update(dt);
if (isGameOver) {
return;
}
for (final child in children) {
if (child is Bullet && child.hasBeenHit && !child.isMine) {
_playerHealthPoint = _playerHealthPoint - child.damage;
final mirroredPosition = _player.getMirroredPercentPosition();
onGameStateUpdate(mirroredPosition, _playerHealthPoint);
_player.updateHealth(_playerHealthPoint / _initialHealthPoints);
}
}
if (_playerHealthPoint <= 0) {
endGame(false);
}
}
void startNewGame() {
isGameOver = false;
_playerHealthPoint = _initialHealthPoints;
for (final child in children) {
if (child is Player) {
child.position = child.initialPosition;
} else if (child is Bullet) {
child.removeFromParent();
}
}
_shootBullets();
}
/// shoots out bullets form both the player and the opponent.
///
/// Calls itself every 500 milliseconds
Future<void> _shootBullets() async {
await Future.delayed(const Duration(milliseconds: 500));
/// Player's bullet
final playerBulletInitialPosition = Vector2.copy(_player.position)
..y -= Player.radius;
final playerBulletVelocities = [
Vector2(0, -100),
Vector2(60, -80),
Vector2(-60, -80),
];
for (final bulletVelocity in playerBulletVelocities) {
add((Bullet(
isMine: true,
velocity: bulletVelocity,
image: _playerBulletImage,
initialPosition: playerBulletInitialPosition,
)));
}
/// Opponent's bullet
final opponentBulletInitialPosition = Vector2.copy(_opponent.position)
..y += Player.radius;
final opponentBulletVelocities = [
Vector2(0, 100),
Vector2(60, 80),
Vector2(-60, 80),
];
for (final bulletVelocity in opponentBulletVelocities) {
add((Bullet(
isMine: false,
velocity: bulletVelocity,
image: _opponentBulletImage,
initialPosition: opponentBulletInitialPosition,
)));
}
_shootBullets();
}
void updateOpponent({required Vector2 position, required int health}) {
_opponent.position = Vector2(size.x * position.x, size.y * position.y);
_opponent.updateHealth(health / _initialHealthPoints);
}
/// Called when either the player or the opponent has run out of health points
void endGame(bool playerWon) {
isGameOver = true;
onGameOver(playerWon);
}
}
```
There is a lot going here, so let’s break it down. Within the `onLoad` method, we are loading all of the sprites used throughout the game. Then we are adding the player and opponent component.
Within `onPanUpdate`, we handle the user dragging on the screen. Note that we are calling the `onGameStateUpdate` callback to pass the player’s position so that we can share it to the opponent’s client later when we handle network connections. On the other hand, we have the `updateOpponent` method, which is used to update the opponent’s state with the information coming in from the network. We will also add logic to call it from the Flutter widgets later.
Upon starting the game, `_shootBullets()` is called, which shoots out bullets both from the player and the opponent. `_shootBullets()` is a recursive function that calls itself every 500 milliseconds. If the bullet hits the player, it is caught inside the `udpate()` method, which is called on every frame. There we calculate the new player’s health points.
### Creating the Player Component
`Player` component has the UFO sprite and represents the player and the opponent. It extends the `PositionComponent` from Flame. Add the following in `lib/player.dart`
```dart
import 'dart:async';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame_realtime_shooting/game/bullet.dart';
import 'package:flutter/material.dart';
class Player extends PositionComponent with HasGameRef, CollisionCallbacks {
Vector2 velocity = Vector2.zero();
late final Vector2 initialPosition;
Player({required bool isMe}) : _isMyPlayer = isMe;
/// Whether it's me or the opponent
final bool _isMyPlayer;
static const radius = 30.0;
@override
Future<void>? onLoad() async {
anchor = Anchor.center;
width = radius * 2;
height = radius * 2;
final initialX = gameRef.size.x / 2;
initialPosition = _isMyPlayer
? Vector2(initialX, gameRef.size.y * 0.8)
: Vector2(initialX, gameRef.size.y * 0.2);
position = initialPosition;
add(CircleHitbox());
add(_Gauge());
await super.onLoad();
}
void move(Vector2 delta) {
position += delta;
}
void updateHealth(double healthLeft) {
for (final child in children) {
if (child is _Gauge) {
child._healthLeft = healthLeft;
}
}
}
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is Bullet && _isMyPlayer != other.isMine) {
other.hasBeenHit = true;
other.removeFromParent();
}
}
/// returns the mirrored percent position of the player
/// to be broadcasted to other clients
Vector2 getMirroredPercentPosition() {
final mirroredPosition = gameRef.size - position;
return Vector2(mirroredPosition.x / gameRef.size.x,
mirroredPosition.y / gameRef.size.y);
}
}
class _Gauge extends PositionComponent {
double _healthLeft = 1.0;
@override
FutureOr<void> onLoad() {
final playerParent = parent;
if (playerParent is Player) {
width = playerParent.width;
height = 10;
anchor = Anchor.centerLeft;
position = Vector2(0, 0);
}
return super.onLoad();
}
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(
Rect.fromPoints(
const Offset(0, 0),
Offset(width, height),
),
Paint()..color = Colors.white);
canvas.drawRect(
Rect.fromPoints(
const Offset(0, 0),
Offset(width * _healthLeft, height),
),
Paint()
..color = _healthLeft > 0.5
? Colors.green
: _healthLeft > 0.25
? Colors.orange
: Colors.red);
}
}
```
You can see that it has a `_isMyPlayer` property, which is true for the player and false for the opponent. If we take a look at the `onLoad` method, we can use this to position it either at the top if it’s the opponent, or at the bottom if it’s the player. We can also see that we are adding a `CircleHitbox`, because we need to detect collisions between it and the bullets. Lastly, we are adding `_Gauge` as its child, which is the health point gauge you see on top of each players. Within `onCollision` callback, we are checking if the collided object is the opponent’s bullet, and if it is, we flag the bullet as `hasBeenHit` and remove it from the game.
`getMirroredPercentPosition` method is used when sharing the position with the opponent’s client. It calculates the mirrored position of the player. `updateHealth` is called when the health changes and updates the bar length of the `_Gauge` class.
### Adding bullets
Lastly we will add the `Bullet` class. It represents a single bullet coming out from the player and the opponent. Within `onLoad` it adds the sprite to apply the nice image and `CircleHitbox` so that it can collide with other objects. You can also see that it receives a `velocity` in the constructor, the position is updated using the velocity and the elapsed time within the `update` method. This is how you can have it move in a single direction at a constant speed.
```dart
import 'dart:async';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/image_composition.dart' as flame_image;
class Bullet extends PositionComponent with CollisionCallbacks, HasGameRef {
final Vector2 velocity;
final flame_image.Image image;
static const radius = 5.0;
bool hasBeenHit = false;
final bool isMine;
/// Damage that it deals when it hits the player
final int damage = 5;
Bullet({
required this.isMine,
required this.velocity,
required this.image,
required Vector2 initialPosition,
}) : super(position: initialPosition);
@override
Future<void>? onLoad() async {
anchor = Anchor.center;
width = radius * 2;
height = radius * 2;
add(CircleHitbox()
..collisionType = CollisionType.passive
..anchor = Anchor.center);
final sprite =
SpriteComponent.fromImage(image, size: Vector2.all(radius * 2));
add(sprite);
await super.onLoad();
}
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
if (position.y < 0 || position.y > gameRef.size.y) {
removeFromParent();
}
}
}
```
### Step 4. Add real-time communications between players
At this point, we have a working shooting game except the opponent does not move, because we have not added any ways to communicate between clients. We will use Supabase’s [realtime features](https://supabase.com/docs/guides/realtime) for this, because it gives us an out of the box solution to handle low-latency real-time communication between players. If you do not have a Supabase project created yet, head over to [database.new](https://database.new/) to create one.
Before we get into any coding, let’s install the Supabase SDK into our app. We will also use the [uuid package](https://pub.dev/packages/uuid) to generate random unique ids for the users. Add the following to `pubspec.yaml` file.
```yaml
supabase_flutter: ^1.4.0
uuid: ^3.0.7
```
Once `pub get` is complete, let’s initialize Supabase. We will override the `main` function to initialize Supabase. You can get your Supabase URL and Anon Key at `Project Setting` > `API`. Copy and paste them into the `Supabase.initialize` call.
```dart
void main() async {
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_ANON_KEY',
realtimeClientOptions: const RealtimeClientOptions(eventsPerSecond: 40),
);
runApp(const MyApp());
}
// Extract Supabase client for easy access to Supabase
final supabase = Supabase.instance.client;
```
`RealtimeClientOptions` here is a parameter to override how many events per second each client will send to Supabase. The default is 10, but we want to override to 40 to provide a more in-synced experience.
With this, we are ready to get into adding the real-time features now.
### Handle the Lobby to wait for Other Players to show up
We will start by rewriting the `_Lobby` class the first thing we have to do in the lobby is to wait and detect other online users also at the lobby. We can implement this using the [presence](https://supabase.com/docs/guides/realtime/presence) feature in Supabase.
Add `initState` and inside it initialize a `RealtimeChannel` instance. We can call it `_lobbyChannel`. If we take a look at the `subscribe()` method, we can see that upon successful subscription to the lobby channel, we are tracking our the unique user ID that we create uplon initialization. We are listening to the `sync` event to get notified whenever anyone is “present”. Within the callback, we are extracting the userIds of all the users in the lobby and set it as the state.A game starts when someone taps on the `Start` button. If we take a look at the `onPressed` callback, we see that we are sending a [broadcast](https://supabase.com/docs/guides/realtime/broadcast) event to the lobby channel with two participant ids and a randomly generated game ID. [Broadcast](https://supabase.com/docs/guides/realtime/broadcast) is a feature of Supabase to send and receive lightweight low-latency data between clients, and when the two participants, one of them being the person tapping on the `start` button, is received on both ends, a game starts. We can observe within `initState` inside the callback for `game_start` event that upon receiving a broadcast event, it checks if the player is one of the participants, and if it is, it will call the `onGameStarted` call back and pop the navigator removing the dialog. The game has begun!
```dart
class _LobbyDialogState extends State<_LobbyDialog> {
List<String> _userids = [];
bool _loading = false;
/// Unique identifier for each players to identify eachother in lobby
final myUserId = const Uuid().v4();
late final RealtimeChannel _lobbyChannel;
@override
void initState() {
super.initState();
_lobbyChannel = supabase.channel(
'lobby',
opts: const RealtimeChannelConfig(self: true),
);
_lobbyChannel.on(RealtimeListenTypes.presence, ChannelFilter(event: 'sync'),
(payload, [ref]) {
// Update the lobby count
final presenceState = _lobbyChannel.presenceState();
setState(() {
_userids = presenceState.values
.map((presences) =>
(presences.first as Presence).payload['user_id'] as String)
.toList();
});
}).on(RealtimeListenTypes.broadcast, ChannelFilter(event: 'game_start'),
(payload, [_]) {
// Start the game if someone has started a game with you
final participantIds = List<String>.from(payload['participants']);
if (participantIds.contains(myUserId)) {
final gameId = payload['game_id'] as String;
widget.onGameStarted(gameId);
Navigator.of(context).pop();
}
}).subscribe(
(status, [ref]) async {
if (status == 'SUBSCRIBED') {
await _lobbyChannel.track({'user_id': myUserId});
}
},
);
}
@override
void dispose() {
supabase.removeChannel(_lobbyChannel);
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Lobby'),
content: _loading
? const SizedBox(
height: 100,
child: Center(child: CircularProgressIndicator()),
)
: Text('${_userids.length} users waiting'),
actions: [
TextButton(
onPressed: _userids.length < 2
? null
: () async {
setState(() {
_loading = true;
});
final opponentId =
_userids.firstWhere((userId) => userId != myUserId);
final gameId = const Uuid().v4();
await _lobbyChannel.send(
type: RealtimeListenTypes.broadcast,
event: 'game_start',
payload: {
'participants': [
opponentId,
myUserId,
],
'game_id': gameId,
},
);
},
child: const Text('start'),
),
],
);
}
}
```
### Sharing Game States with the Opposing Player
Once a game begins, we need to synchronize the game states between the two clients. In our case, we will sync only the player’s position and health points. Whenever a player moves, or the player’s health points change, the `onGameStateUpdate` callback on our `MyGame` instance that will fire notifying the update along with its position and health point. We will broadcast those information to the opponent’s client via Supabase [broadcast feature](https://supabase.com/docs/guides/realtime/broadcast).
Fill in the `_initialize` method like the following to initialize the game.
```dart
class GamePage extends StatefulWidget {
const GamePage({Key? key}) : super(key: key);
@override
State<GamePage> createState() => _GamePageState();
}
class _GamePageState extends State<GamePage> {
late final MyGame _game;
/// Holds the RealtimeChannel to sync game states
RealtimeChannel? _gameChannel;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
Image.asset('assets/images/background.jpg', fit: BoxFit.cover),
GameWidget(game: _game),
],
),
);
}
@override
void initState() {
super.initState();
_initialize();
}
Future<void> _initialize() async {
_game = MyGame(
onGameStateUpdate: (position, health) async {
ChannelResponse response;
// Loop until the send succeeds if the payload is to notify defeat.
do {
response = await _gameChannel!.send(
type: RealtimeListenTypes.broadcast,
event: 'game_state',
payload: {'x': position.x, 'y': position.y, 'health': health},
);
// wait for a frame to avoid infinite rate limiting loops
await Future.delayed(Duration.zero);
setState(() {});
} while (response == ChannelResponse.rateLimited && health <= 0);
},
onGameOver: (playerWon) async {
await showDialog(
barrierDismissible: false,
context: context,
builder: ((context) {
return AlertDialog(
title: Text(playerWon ? 'You Won!' : 'You lost...'),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await supabase.removeChannel(_gameChannel!);
_openLobbyDialog();
},
child: const Text('Back to Lobby'),
),
],
);
}),
);
},
);
// await for a frame so that the widget mounts
await Future.delayed(Duration.zero);
if (mounted) {
_openLobbyDialog();
}
}
void _openLobbyDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return _LobbyDialog(
onGameStarted: (gameId) async {
// await a frame to allow subscribing to a new channel in a realtime callback
await Future.delayed(Duration.zero);
setState(() {});
_game.startNewGame();
_gameChannel = supabase.channel(gameId,
opts: const RealtimeChannelConfig(ack: true));
_gameChannel!.on(RealtimeListenTypes.broadcast,
ChannelFilter(event: 'game_state'), (payload, [_]) {
final position =
Vector2(payload['x'] as double, payload['y'] as double);
final opponentHealth = payload['health'] as int;
_game.updateOpponent(
position: position,
health: opponentHealth,
);
if (opponentHealth <= 0) {
if (!_game.isGameOver) {
_game.isGameOver = true;
_game.onGameOver(true);
}
}
}).subscribe();
},
);
});
}
}
```
You can see that within `_openLobbyDialog`, there is an `onGameStarted` callback for when the game has started. Once a game has been started, it creates a new channel using the game ID as the channel name and starts listening to game state updates from the opponent.You can see that within the `onGameOver` callback, we are showing a simple dialog. Upon tapping `Back to Lobby`, the user will be taken back to the lobby dialog, where they can start another game if they want to.
![Post game dialog](/images/blog/flutter-realtime-game/post-game-dialog.png)
With all of that put together, we have a functioning real-time multiplayer shooting game. Grab a friend, run the app with `flutter run`, and have fun with it!
## Conclusions
We learned how to create an interactive shooting game. We took advantage of Flutter’s dialogs to create a quick and easy lobby and post-game UI. Then we created the game using Flame. We learned how to detect and handle collisions and experienced how easy creating a sophisticated game was using Flame. Finally, we added capabilities to share game states with other clients to complete a real-time multiplayer experience without managing our own infrastructure using [Supabase](https://supabase.com/).
## More Flutter Resources
- [Complete code of this article](https://github.com/supabase/supabase/tree/master/examples/realtime/examples/flutter-multiplayer-shooting-game)
- [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app)
- [Flutter Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls)
- [Generate Flame template using Very Good CLI](https://verygood.ventures/blog/generate-a-game-with-our-new-template)
- [Supabase Flutter SDK docs](https://supabase.com/docs/reference/dart/start)
@@ -3,6 +3,7 @@ import { Button } from 'ui'
import { useState } from 'react'
// Import Swiper React components
import { useRouter } from 'next/router'
import Image from 'next/image'
import { ColorSwatchIcon, MenuIcon } from '@heroicons/react/outline'
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
@@ -120,7 +121,14 @@ function AuthWidgetSection() {
match your brand and aesthetic
</p>
<div className="mb-4 flex items-center space-x-2">
<img className="m-0 w-8" src={`${basePath}/images/product/auth/react-icon.svg`} />
<div className="m-0 w-8 flex items-center">
<Image
src={`${basePath}/images/product/auth/react-icon.svg`}
alt="react icon"
width="100%"
height="100%"
/>
</div>
<small>React only. Other frameworks coming soon.</small>
</div>
</div>
@@ -204,14 +212,22 @@ function AuthWidgetSection() {
: '',
].join(' ')}
>
<img
src="/images/auth-ui/small--light.svg"
className="hidden w-4 text-red-900 dark:block"
/>
<img
src="/images/auth-ui/small--dark.svg"
className="block w-4 text-red-900 dark:hidden"
/>
<div className="m-0 w-4 items-center hidden text-red-900 dark:flex">
<Image
src="/images/auth-ui/small--light.svg"
alt="react icon"
width="100%"
height="100%"
/>
</div>
<div className="m-0 w-4 items-center flex text-red-900 dark:hidden">
<Image
src="/images/auth-ui/small--dark.svg"
alt="react icon"
width="100%"
height="100%"
/>
</div>
</button>
<button
onClick={() => setRadius('10px')}
@@ -222,14 +238,22 @@ function AuthWidgetSection() {
: '',
].join(' ')}
>
<img
src="/images/auth-ui/medium--light.svg"
className="hidden w-4 text-red-900 dark:block"
/>
<img
src="/images/auth-ui/medium--dark.svg"
className="block w-4 text-red-900 dark:hidden"
/>
<div className="m-0 w-4 items-center hidden text-red-900 dark:flex">
<Image
src="/images/auth-ui/medium--light.svg"
alt="react icon"
width="100%"
height="100%"
/>
</div>
<div className="m-0 w-4 items-center flex text-red-900 dark:hidden">
<Image
src="/images/auth-ui/medium--dark.svg"
alt="react icon"
width="100%"
height="100%"
/>
</div>
</button>
<button
onClick={() => setRadius('32px')}
@@ -240,14 +264,22 @@ function AuthWidgetSection() {
: '',
].join(' ')}
>
<img
src="/images/auth-ui/large--dark.svg"
className="hidden w-4 text-red-900 dark:block"
/>
<img
src="/images/auth-ui/large--light.svg"
className="block w-4 text-red-900 dark:hidden"
/>
<div className="m-0 w-4 items-center flex text-red-900 dark:hidden">
<Image
src="/images/auth-ui/large--light.svg"
alt="react icon"
width="100%"
height="100%"
/>
</div>
<div className="m-0 w-4 items-center hidden text-red-900 dark:flex">
<Image
src="/images/auth-ui/large--dark.svg"
alt="react icon"
width="100%"
height="100%"
/>
</div>
</button>
</div>
</div>
@@ -58,6 +58,7 @@ const BlogListItem = ({ post }: Props) => {
<Image
src={author.author_image_url}
className="dark:border-dark rounded-full border"
alt={`${author.author} avatar`}
width="100%"
height="100%"
layout="responsive"
@@ -110,6 +110,7 @@ function ImageCarousel(props: ImageCarouselProps) {
{content.img_url && (
<Image
src={`${basePath}${content.img_url}`}
alt={content.title}
layout="responsive"
width="1460"
height="960"
+2 -6
View File
@@ -91,15 +91,11 @@ const ImageGrid = ({
${x.link && 'dark:hover:bg-scale-600 hover:bg-gray-100'}
p-8 ${className}`}
>
<div
className={`relative h-8 w-full overflow-auto
${imgPadding[padding]}
`}
>
<div className={`relative h-8 w-full overflow-auto ${imgPadding[padding]}`}>
<Image
layout="fill"
src={`${x.image}`}
alt={x.alt}
alt={`${x.name} logo`}
objectFit="scale-down"
objectPosition="center"
className="
+2 -2
View File
@@ -21,8 +21,8 @@
}
},
{
"title": "Database space",
"tooltip": "Billing is based on the average database size in GB throughout the billing period.",
"title": "Database size",
"tooltip": "Billing is based on the average daily database size in GB throughout the billing period.",
"tiers": {
"free": "Up to 500 MB",
"pro": "8 GB included, then $0.125 per GB",
+1 -1
View File
@@ -149,7 +149,7 @@
"author": "Tyler Shukert",
"username": "dshukertjr",
"position": "DevRel & Flutter",
"author_url": "https://github.com/dshukertjr",
"author_url": "https://twitter.com/dshukertjr",
"author_image_url": "https://github.com/dshukertjr.png"
},
{
+1
View File
@@ -166,6 +166,7 @@ function FeaturedThumb(blog: PostTypes) {
src={`/images/blog/` + (blog.thumb ? blog.thumb : blog.image)}
layout="fill"
objectFit="cover"
alt="blog thumbnail"
/>
</div>
<div className="flex flex-col space-y-2">
+2
View File
@@ -209,6 +209,7 @@ function BlogPostPage(props: any) {
<Image
src={author.author_image_url}
className="dark:border-dark rounded-full border"
alt={`${author.author} avatar`}
width="100%"
height="100%"
layout="responsive"
@@ -253,6 +254,7 @@ function BlogPostPage(props: any) {
<div className="relative mb-8 h-96 w-full overflow-auto rounded-lg border">
<Image
src={'/images/blog/' + props.blog.thumb}
alt={props.blog.title}
layout="fill"
objectFit="cover"
/>
+6 -1
View File
@@ -49,7 +49,12 @@ const Index = () => {
<SectionContainer>
<div className="shadow-small grid grid-cols-12 rounded-lg border dark:border-gray-600">
<div className="relative col-span-12 h-60 w-full overflow-auto rounded-lg lg:col-span-5">
<Image src={supabaseLogoPreview} layout="fill" objectFit="cover" />
<Image
src={supabaseLogoPreview}
alt="Supabase logo Preview"
layout="fill"
objectFit="cover"
/>
</div>
<div className="col-span-12 flex items-center lg:col-span-7">
<div className="p-16">
+264 -209
View File
@@ -1,5 +1,4 @@
// @ts-nocheck
import { NextSeo } from 'next-seo'
import _days from '~/components/LaunchWeek/lw6_days.json'
@@ -19,6 +18,7 @@ import { WeekDayProps } from '~/components/LaunchWeek/types'
import styles from './launchWeek.module.css'
import Image from 'next/image'
import Link from 'next/link'
const days = _days as WeekDayProps[]
const constellation = [
@@ -122,33 +122,51 @@ export default function launchweek() {
const SectionButtons = ({ blog, docs, video }) => {
return (
<div className="flex gap-2 z-10">
<a href={blog} target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Blog post
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img src="/images/launchweek/icons-blogpost.svg" className="w-4 h-4"></img>
</div>
</div>
</a>
{docs && (
<a href={docs} target="_blank" rel="noopener">
<Link href={blog}>
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Docs
Blog post
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img src="/images/launchweek/icons-docs.svg" className="w-4 h-4"></img>
<img
src="/images/launchweek/icons-blogpost.svg"
className="w-4 h-4"
alt="blog post icon"
/>
</div>
</div>
</a>
</Link>
{docs && (
<Link href={docs}>
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Docs
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/icons-docs.svg"
className="w-4 h-4"
alt="docs icon"
/>
</div>
</div>
</a>
</Link>
)}
{video && (
<a href={video} target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Video
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img src="/images/launchweek/video-icon.svg" className="w-4 h-4"></img>
<Link href={video}>
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Video
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-4 h-4"
alt="video icon"
/>
</div>
</div>
</div>
</a>
</a>
</Link>
)}
</div>
)
@@ -181,12 +199,11 @@ export default function launchweek() {
>
<div className="flex justify-center">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="flex w-40 dark:hidden lg:w-80"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="hidden w-40 dark:flex lg:w-80"
src={`/images/launchweek/${
isDarkMode ? 'launchweek-logo--dark.svg' : 'launchweek-logo--lights.svg'
}`}
className="flex w-40 lg:w-80"
alt="Launch Week logo"
/>
</div>
<p className="text-scale-1100 text-sm text-center">Dec 12 – 16 at 6 AM PT | 9 AM ET</p>
@@ -228,9 +245,10 @@ export default function launchweek() {
<img
src={`/images/launchweek/antcopplecall.png`}
className="brightness-125"
alt="Ant & Copple"
width={120}
height={80}
></img>
/>
</div>
<div className="flex flex-col lg:flex-row ml-8 sm:ml-10">
<span className="text-black dark:text-white mr-2">Who we hire at Supabase</span>
@@ -238,22 +256,34 @@ export default function launchweek() {
</div>
</div>
<div className="flex gap-2 z-10">
<a href={'https://youtu.be/-BG9XptyCKI'} target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[130px]">
Watch video
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img src="/images/launchweek/video-icon.svg" className="w-3 h-3"></img>
<Link href="https://youtu.be/-BG9XptyCKI">
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[130px]">
Watch video
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-3 h-3"
alt="video icon"
/>
</div>
</div>
</div>
</a>
<a href={'/blog/who-we-hire'} target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[142px]">
Read blogpost
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img src="/images/launchweek/icons-blogpost.svg" className="w-3 h-3"></img>
</a>
</Link>
<Link href="/blog/who-we-hire">
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[142px]">
Read blogpost
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/icons-blogpost.svg"
className="w-3 h-3"
alt="blog post icon"
/>
</div>
</div>
</div>
</a>
</a>
</Link>
</div>
</div>
</SectionContainer>
@@ -263,9 +293,10 @@ export default function launchweek() {
<div className="flex min-w-[150px]">
<img
src={`/images/launchweek/${isDarkMode ? 'outro.svg' : 'outro-light.svg'}`}
alt="Outro icon"
width={110}
height={80}
></img>
/>
</div>
<div className="flex flex-col lg:flex-row ml-8 sm:ml-10">
<span className="text-black dark:text-white mr-2">Wrap Up</span>
@@ -273,14 +304,20 @@ export default function launchweek() {
</div>
</div>
<div className="flex gap-2 z-10">
<a href={'/blog/launch-week-6-wrap-up'} target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[142px]">
Read blogpost
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img src="/images/launchweek/icons-blogpost.svg" className="w-3 h-3"></img>
<Link href="/blog/launch-week-6-wrap-up">
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[142px]">
Read blogpost
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/icons-blogpost.svg"
className="w-3 h-3"
alt="blog post icon"
/>
</div>
</div>
</div>
</a>
</a>
</Link>
</div>
</div>
</SectionContainer>
@@ -433,6 +470,7 @@ export default function launchweek() {
? '/images/launchweek/mfa-dark.png'
: '/images/launchweek/mfa-light.png'
}
alt="MFA"
layout="fill"
objectFit="cover"
quality={100}
@@ -446,6 +484,7 @@ export default function launchweek() {
? '/images/launchweek/mfa-dark-hover.png'
: '/images/launchweek/mfa-light-hover.png'
}
alt="MFA"
layout="fill"
objectFit="cover"
quality={100}
@@ -492,6 +531,7 @@ export default function launchweek() {
? '/images/launchweek/wrappers-visual.svg'
: '/images/launchweek/wrappers-visual-light.svg'
}
alt="Wrappers"
layout="fill"
objectFit="cover"
quality={100}
@@ -508,6 +548,7 @@ export default function launchweek() {
? '/images/launchweek/wrappers-visual-hover.svg'
: '/images/launchweek/wrappers-visual-hover-light.svg'
}
alt="Wrappers"
layout="fill"
objectFit="cover"
quality={100}
@@ -559,6 +600,7 @@ export default function launchweek() {
? '/images/launchweek/vault-visual.svg'
: '/images/launchweek/vault-visual-light.svg'
}
alt="Vault"
layout="fill"
objectFit="cover"
quality={100}
@@ -575,6 +617,7 @@ export default function launchweek() {
? '/images/launchweek/vault-visual-hover.svg'
: '/images/launchweek/vault-visual-hover-light.svg'
}
alt="Vault"
layout="fill"
objectFit="cover"
quality={100}
@@ -601,6 +644,7 @@ export default function launchweek() {
? '/images/launchweek/TCE-visual.svg'
: '/images/launchweek/TCE-visual-light.svg'
}
alt="TCE"
layout="fill"
objectFit="cover"
quality={100}
@@ -617,6 +661,7 @@ export default function launchweek() {
? '/images/launchweek/TCE-visual-hover.svg'
: '/images/launchweek/TCE-visual-hover-light.svg'
}
alt="TCE"
layout="fill"
objectFit="cover"
quality={100}
@@ -648,6 +693,7 @@ export default function launchweek() {
? '/images/launchweek/community-visual.svg'
: '/images/launchweek/community-visual-light.svg'
}
alt="Community"
layout="fill"
objectFit="cover"
quality={100}
@@ -664,6 +710,7 @@ export default function launchweek() {
? '/images/launchweek/community-visual-hover.svg'
: '/images/launchweek/community-visual-light-hover.svg'
}
alt="Community"
layout="fill"
objectFit="cover"
quality={100}
@@ -675,68 +722,64 @@ export default function launchweek() {
</div>
<div className="flex flex-col md:flex-row gap-4 md:gap-2 z-10 ">
<div className="flex gap-4 md:gap-2">
<a
href={'/blog/launch-week-6-community-day'}
target="_blank"
rel="noopener"
>
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Blog post
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/icons-blogpost.svg"
className="w-4 h-4"
></img>
<Link href="/blog/launch-week-6-community-day">
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Blog post
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/icons-blogpost.svg"
className="w-4 h-4"
alt="blog post icon"
/>
</div>
</div>
</div>
</a>
<a
href={'https://www.youtube.com/watch?v=hw9Q-NjASbU'}
target="_blank"
rel="noopener"
>
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Flutterflow
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-4 h-4"
></img>
</a>
</Link>
<Link href="https://www.youtube.com/watch?v=hw9Q-NjASbU">
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
Flutterflow
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-4 h-4"
alt="video icon"
/>
</div>
</div>
</div>
</a>
</a>
</Link>
</div>
<div className="flex gap-4 md:gap-2">
<a
href={'https://www.youtube.com/watch?v=mw0DLwItue4'}
target="_blank"
rel="noopener"
>
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
OneSignal
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-4 h-4"
></img>
<Link href="https://www.youtube.com/watch?v=mw0DLwItue4">
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
OneSignal
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-4 h-4"
alt="video icon"
/>
</div>
</div>
</div>
</a>
<a
href={'https://www.youtube.com/watch?v=EdYQ9fF-hz4'}
target="_blank"
rel="noopener"
>
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
NextAuth
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-4 h-4"
></img>
</a>
</Link>
<Link href="https://www.youtube.com/watch?v=EdYQ9fF-hz4">
<a target="_blank" rel="noopener">
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
NextAuth
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
<img
src="/images/launchweek/video-icon.svg"
className="w-4 h-4"
alt="video icon"
/>
</div>
</div>
</div>
</a>
</a>
</Link>
</div>
</div>
</div>
@@ -755,6 +798,7 @@ export default function launchweek() {
? '/images/launchweek/PgGraphql-visual.svg'
: '/images/launchweek/PgGraphql-visual-light.svg'
}
alt="PgGraphql"
layout="fill"
objectFit="cover"
quality={100}
@@ -771,6 +815,7 @@ export default function launchweek() {
? '/images/launchweek/PgGraphql-visual-hover.svg'
: '/images/launchweek/PgGraphql-visual-hover-light.svg'
}
alt="PgGraphql"
layout="fill"
objectFit="cover"
quality={100}
@@ -800,6 +845,7 @@ export default function launchweek() {
? '/images/launchweek/custom-domains-visual.svg'
: '/images/launchweek/custom-domains-visual-light.svg'
}
alt="Custom Domains"
layout="fill"
objectFit="cover"
quality={100}
@@ -816,6 +862,7 @@ export default function launchweek() {
? '/images/launchweek/custom-domains-visual-hover.svg'
: '/images/launchweek/custom-domains-visual-hover-light.svg'
}
alt="Custom Domains"
layout="fill"
objectFit="cover"
quality={100}
@@ -845,6 +892,7 @@ export default function launchweek() {
? '/images/launchweek/PITR-visual.svg'
: '/images/launchweek/PITR-visual-light.svg'
}
alt="PITR"
layout="fill"
objectFit="cover"
quality={100}
@@ -861,6 +909,7 @@ export default function launchweek() {
? '/images/launchweek/PITR-visual-hover.svg'
: '/images/launchweek/PITR-visual-hover-light.svg'
}
alt="PITR"
layout="fill"
objectFit="cover"
quality={100}
@@ -890,6 +939,7 @@ export default function launchweek() {
? '/images/launchweek/pg_crdt-visual.svg'
: '/images/launchweek/pg_crdt-visual-light.svg'
}
alt="pg_crdt"
layout="fill"
objectFit="cover"
quality={100}
@@ -906,6 +956,7 @@ export default function launchweek() {
? '/images/launchweek/pg_crdt-visual-hover.svg'
: '/images/launchweek/pg_crdt-visual-hover-light.svg'
}
alt="pg_crdt"
layout="fill"
objectFit="cover"
quality={100}
@@ -935,6 +986,7 @@ export default function launchweek() {
? '/images/launchweek/postgres-visual.svg'
: '/images/launchweek/postgres-visual-light.svg'
}
alt="postgres"
layout="fill"
objectFit="cover"
quality={100}
@@ -951,6 +1003,7 @@ export default function launchweek() {
? '/images/launchweek/postgres-visual-hover.svg'
: '/images/launchweek/postgres-visual-hover-light.svg'
}
alt="postgres"
layout="fill"
objectFit="cover"
quality={100}
@@ -980,6 +1033,7 @@ export default function launchweek() {
? '/images/launchweek/PostgREST11-visual.svg'
: '/images/launchweek/PostgREST11-visual-light.svg'
}
alt="PostgREST11"
layout="fill"
objectFit="cover"
quality={100}
@@ -996,6 +1050,7 @@ export default function launchweek() {
? '/images/launchweek/PostgREST11-visual-hover.svg'
: '/images/launchweek/PostgREST11-visual-hover-light.svg'
}
alt="PostgREST11"
layout="fill"
objectFit="cover"
quality={100}
@@ -1039,14 +1094,11 @@ export default function launchweek() {
The traditional parallel Hackathon is back! Build a new open source project with
Supabase and you can win $1500 in GitHub sponsorships and a coveted Supabase Darkmode
Keyboard! For more info check the{' '}
<a
href="https://supabase.com/blog/launch-week-6-hackathon"
target="_blank"
rel="nooper noreferrer"
className="text-brand-900"
>
blog post
</a>
<Link href="https://supabase.com/blog/launch-week-6-hackathon">
<a target="_blank" rel="nooper noreferrer" className="text-brand-900">
blog post
</a>
</Link>
.
</p>
</div>
@@ -1074,27 +1126,27 @@ export default function launchweek() {
or if you just want to chill and watch people build, come and join us!
</p>
<div></div>
<a
href="https://discord.supabase.com/"
rel="noopener noreferrer"
className="text-brand-900 flex items-center"
target="_blank"
>
Join our Discord <IconExternalLink size="small" className="inline-block ml-2" />
</a>
<Link href="https://discord.supabase.com/">
<a
rel="noopener noreferrer"
className="text-brand-900 flex items-center"
target="_blank"
>
Join our Discord{' '}
<IconExternalLink size="small" className="inline-block ml-2" />
</a>
</Link>
</div>
</div>
<div className={'flex flex-col'}>
<h3 className="text-lg text-black dark:text-white mb-2">Submission</h3>
<p className="text-slate-900 w-[90%] lg:w-[80%]">
Submit your project through{' '}
<a
className="text-brand-900"
href="https://www.madewithsupabase.com/launch-week-6"
target="_blank"
>
madewithsupabase.com
</a>
<Link href="https://www.madewithsupabase.com/launch-week-6">
<a className="text-brand-900" target="_blank">
madewithsupabase.com
</a>
</Link>
. All submissions must be open source and publically available. Submissions close
Monday 19th Dec 00:01 AM PT.
</p>
@@ -1106,75 +1158,73 @@ export default function launchweek() {
'flex basis-1/3 flex-col px-5'
)}
>
<a
href="https://github.com/psteinroe/supabase-cache-helpers"
target="_blank"
rel="noopener noreferrer"
className="self-end"
>
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
></img>
<h3 className="text-black dark:text-white">Supabase Cache Helpers</h3>
<p className="text-slate-1000 text-xs">Previous Best Overall Project Winner</p>
</div>
</a>
<a
href="https://github.com/pheralb/superui"
target="_blank"
rel="noopener noreferrer"
>
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
></img>
<h3 className="text-black dark:text-white">Super UI</h3>
<p className="text-slate-1000 text-xs">Previous Best Overall Project Runner Up</p>
</div>
</a>
<a
href="https://github.com/Myzel394/quid_faciam_hodie"
target="_blank"
rel="noopener noreferrer"
className="self-end"
>
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
></img>
<h3 className="text-black dark:text-white">Quid Faciam Hodie?</h3>
<p className="text-slate-1000 text-xs">Winner Best Flutter Project</p>
</div>
</a>
<a href="https://github.com/laznic/hotdogs" target="_blank" rel="noopener noreferrer">
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
></img>
<h3 className="text-black dark:text-white">That Hot Dog Game</h3>
<p className="text-slate-1000 text-xs">Previous Winner: Most Fun/Interesting</p>
</div>
</a>
<a
href="https://github.com/vvidday/repo-watch"
target="_blank"
rel="noopener noreferrer"
className="self-end"
>
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
></img>
<h3 className="text-black dark:text-white">RepoWatch</h3>
<p className="text-slate-1000 text-xs">Previous Winner Best Realtime Project</p>
</div>
</a>
<Link href="https://github.com/psteinroe/supabase-cache-helpers">
<a target="_blank" rel="noopener noreferrer" className="self-end">
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
alt="link icon"
/>
<h3 className="text-black dark:text-white">Supabase Cache Helpers</h3>
<p className="text-slate-1000 text-xs">Previous Best Overall Project Winner</p>
</div>
</a>
</Link>
<Link href="https://github.com/pheralb/superui">
<a target="_blank" rel="noopener noreferrer">
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
alt="link icon"
/>
<h3 className="text-black dark:text-white">Super UI</h3>
<p className="text-slate-1000 text-xs">
Previous Best Overall Project Runner Up
</p>
</div>
</a>
</Link>
<Link href="https://github.com/Myzel394/quid_faciam_hodie">
<a target="_blank" rel="noopener noreferrer" className="self-end">
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
alt="link icon"
/>
<h3 className="text-black dark:text-white">Quid Faciam Hodie?</h3>
<p className="text-slate-1000 text-xs">Winner Best Flutter Project</p>
</div>
</a>
</Link>
<Link href="https://github.com/laznic/hotdogs">
<a target="_blank" rel="noopener noreferrer">
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 mb-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
alt="link icon"
/>
<h3 className="text-black dark:text-white">That Hot Dog Game</h3>
<p className="text-slate-1000 text-xs">Previous Winner: Most Fun/Interesting</p>
</div>
</a>
</Link>
<Link href="https://github.com/vvidday/repo-watch">
<a target="_blank" rel="noopener noreferrer" className="self-end">
<div className="border border-slate-400 dark:border-[#2E2E2E] rounded-xl pl-5 pr-9 py-4 bg-white dark:bg-[#121212] w-fit relative max-w-[250px]">
<img
src="/images/launchweek/link.svg"
className="absolute top-[16px] right-[10px] text-brand-900"
alt="link icon"
/>
<h3 className="text-black dark:text-white">RepoWatch</h3>
<p className="text-slate-1000 text-xs">Previous Winner Best Realtime Project</p>
</div>
</a>
</Link>
</div>
</div>
</SectionContainer>
@@ -1202,12 +1252,15 @@ export default function launchweek() {
>
<div className="relative group">
<div className="absolute -inset-0.5 bg-brand-1100 rounded-full opacity-75 group-hover:opacity-100 group-hover:blur-sm transition duration-500"></div>
<a href={creator.link} target="_blank" rel="noopener">
<img
className="relative rounded-full w-12 h-12 border border-brand-900 hover:shadow-md"
src={creator.profile_picture}
/>
</a>
<Link href={creator.link}>
<a target="_blank" rel="noopener">
<img
className="relative rounded-full w-12 h-12 border border-brand-900 hover:shadow-md"
src={creator.profile_picture}
alt={`${creator.first_name} ${creator.last_name} avatar`}
/>
</a>
</Link>
</div>
</div>
)
@@ -1227,12 +1280,14 @@ export default function launchweek() {
<p className="text-slate-900 md:max-w-[80%] mb-16">
We worked with more than 30 content creators from around the world to drop a mountain
of content simultaneously!
<a rel="noopener" target="_blank" href="/blog/the-supabase-content-storm">
<div className="text-brand-900 flex items-center mt-2">
See all the content
<IconExternalLink size="small" className="inline-block ml-1" />
</div>
</a>
<Link href="/blog/the-supabase-content-storm">
<a rel="noopener" target="_blank">
<div className="text-brand-900 flex items-center mt-2">
See all the content
<IconExternalLink size="small" className="inline-block ml-1" />
</div>
</a>
</Link>
</p>
{activeCreator !== null && (
<div className="lg:max-w-[50%] min-h-[120px]">
+2 -2
View File
@@ -115,12 +115,12 @@ function Partner({ partner }: { partner: Partner }) {
{partner.video && (
<div
className="bg-scale-1000 relative w-full rounded-b-md shadow-lg"
className="bg-scale-1000 relative w-full rounded-md shadow-lg"
style={{ padding: '56.25% 0 0 0', marginBottom: '1rem' }}
>
<iframe
title="Demo video showcasing Supabase"
className="absolute h-full w-full rounded-b-md"
className="absolute h-full w-full rounded-md"
src={`https://www.youtube-nocookie.com/embed/${partner.video}?autoplay=0&loop=0&controls=1&modestbranding=1&rel=0&disablekb=1`}
style={{ top: 0, left: 0 }}
frameBorder="0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@@ -2,7 +2,7 @@ import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
const router = new Router()
router
// Note: path will be prefixed with function name
// Note: path should be prefixed with function name
.get('/oak-server', (context) => {
context.response.body = 'This is an example Oak server running on Edge Functions!'
})
@@ -0,0 +1,2 @@
#Use PolyScale connection string with application_name parameter to get code-free database caching
DATABASE_URL="postgresql:/DB_USERNAME:DB_PASSWORD@psedge.global:5432/postgres?application_name=POLYSCALE_CACHE_ID"
@@ -0,0 +1,18 @@
# postgres-on-the-edge
This function allows you to access your Supabase database directly via TCP from an edge function.
You can add a global cache to your database for fast access to your data, globally by simply connecting your Supabase database to [PolyScale](https://polyscale.ai).
## Setup
1. Rename `.env.example` to `.env`
2. Insert Supabase database connection string for `DATABASE_URL` in `.env` file
3. (Optional) Create PolyScale cache - see [instructions](https://supabase.com/docs/guides/integrations/polyscale) for adding a PolyScale cache.
4. Replace `DATABASE_URL` with PolyScale connection string.
## Deploy
1. Run `supabase functions deploy --no-verify-jwt postgres-on-the-edge`
2. Run `supabase secrets set --env-file supabase/functions/postgres-on-the-edge/.env`
Learn more about creating this function [here](https://www.youtube.com/watch?v=cl7EuF1-RsY).
@@ -0,0 +1,43 @@
import * as postgres from 'https://deno.land/x/postgres@v0.14.2/mod.ts'
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
// Get the connection string from the environment variable "DATABASE_URL"
const databaseUrl = Deno.env.get('DATABASE_URL')!
// Create a database pool with three connections that are lazily established
const pool = new postgres.Pool(databaseUrl, 3, true)
serve(async (_req) => {
try {
// Grab a connection from the pool
const connection = await pool.connect()
try {
// Run a query
const result = await connection.queryObject`SELECT * FROM animals`
const animals = result.rows // [{ id: 1, name: "Lion" }, ...]
console.log(animals)
// Encode the result as pretty printed JSON
const body = JSON.stringify(
animals,
(_key, value) => (typeof value === 'bigint' ? value.toString() : value),
2
)
// Return the response with the correct content type header
return new Response(body, {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
})
} finally {
// Release the connection back into the pool
connection.release()
}
} catch (err) {
console.error(err)
return new Response(String(err?.message ?? err), { status: 500 })
}
})
@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
/pubspec.lock
@@ -0,0 +1,48 @@
# Flutter Real-time Multiplayer Shooting Game
A real-time shooting game built with [Flutter](https://flutter.dev/), [Flame](https://flame-engine.org/) and [Supabase](https://supabase.com).
You can find a step by step guide on how to build this app on [How to build a real-time multiplayer game with Flutter Flame](https://supabase.com/blog/flutter-real-time-multiplayer-game) article.
## Getting Started
### 1. Create new project
Sign up to Supabase - [app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
### 2. Get the URL and Key
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token.
![Supabase Anon Key](supabase_anon_key.jpg?raw=true 'Supabase Anon Key')
### 3. Pull this example git repository
`git clone <<this repository url>> `
### 4. Paste the Supabase URL and Anon Key
Copy and paste the Supabase URL and Anon key in `lib/main.dart` file
```dart
void main() async {
await Supabase.initialize(
url: 'supabaseUrl',
anonKey: 'supabaseAnonKey',
realtimeClientOptions: const RealtimeClientOptions(eventsPerSecond: 40),
);
runApp(const MyApp());
}
```
### 5. Run the Flutter App
Now run
```bash
flutter run
```
This app can run on any environment that runs Flutter.
@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,71 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flame_realtime_shooting"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.flame_realtime_shooting">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,34 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.flame_realtime_shooting">
<application
android:label="flame_realtime_shooting"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
@@ -0,0 +1,6 @@
package com.example.flame_realtime_shooting
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.flame_realtime_shooting">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
</dict>
</plist>
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff. Show more