Merge branch 'master' into feat/minutely-daily-usage-charts

This commit is contained in:
TzeYiing committed 2022-01-12 03:22:01 +08:00
commit f140f435fe
23 files changed
+690 -361

No files matched your search

+484 -302
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -10,7 +10,7 @@
"@docusaurus/core": "^2.0.0-alpha.63",
"@docusaurus/preset-classic": "^2.0.0-alpha.63",
"@kiwicopple/prism-react-renderer": "github:kiwicopple/prism-react-renderer",
"mermaid": "^8.6.4",
"mermaid": "^8.13.8",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-inlinesvg": "^2.0.1"
+2 -2
View File
@@ -3,8 +3,8 @@
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
ANON_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYyNzIwODU0MCwiZXhwIjoxOTc0MzYzNzQwfQ.zcaQfHd3VA7XgJmdGfmV86OLVJT9s2MTmSy-e69BpUY
SERVICE_ROLE_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjI3MjA4NTQwLCJleHAiOjE5NzQzNjM3NDB9.pkT3PNpO4DtO45Ac5HK_TKCx8sGLgNtV__pr_ZrRSAU
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
# Auth
+2 -2
View File
@@ -6,10 +6,10 @@ _format_version: "1.1"
consumers:
- username: anon
keyauth_credentials:
- key: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYyNzIwODU0MCwiZXhwIjoxOTc0MzYzNzQwfQ.zcaQfHd3VA7XgJmdGfmV86OLVJT9s2MTmSy-e69BpUY
- key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
- username: service_role
keyauth_credentials:
- key: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjI3MjA4NTQwLCJleHAiOjE5NzQzNjM3NDB9.pkT3PNpO4DtO45Ac5HK_TKCx8sGLgNtV__pr_ZrRSAU
- key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
###
### Access Control List
+2 -2
View File
@@ -2,5 +2,5 @@ STUDIO_PG_META_URL=http://localhost:8000/pg
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
SUPABASE_URL=http://localhost:8000
SUPABASE_ANON_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYyNzIwODU0MCwiZXhwIjoxOTc0MzYzNzQwfQ.zcaQfHd3VA7XgJmdGfmV86OLVJT9s2MTmSy-e69BpUY
SUPABASE_SERVICE_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjI3MjA4NTQwLCJleHAiOjE5NzQzNjM3NDB9.pkT3PNpO4DtO45Ac5HK_TKCx8sGLgNtV__pr_ZrRSAU
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
@@ -7,6 +7,7 @@ import {
Popover,
Typography,
} from '@supabase/ui'
import { useRouter } from 'next/router'
import { FC } from 'react'
import Link from 'next/link'
import Image from 'next/image'
@@ -16,6 +17,10 @@ import SVG from 'react-inlinesvg'
interface Props {}
const HelpPopover: FC<Props> = () => {
const router = useRouter()
const projectRef = router.query.ref
const supportUrl = `/support/new${projectRef ? `?ref=${projectRef}` : ''}`
return (
<Popover
className="w-80"
@@ -34,7 +39,7 @@ const HelpPopover: FC<Props> = () => {
hosted services.
</p>
</Typography.Text>
<Link href="/support/new">
<Link href={supportUrl}>
<Button className="sbui-default-button--dark-white" icon={<IconMail />}>
Contact support team
</Button>
@@ -16,7 +16,7 @@ export async function fetchVercelProjects({
url = `${url}?${query}`
}
const response = await get(url, {
headers: { Authorization: `Bearer ${vercelToken}` },
headers: { vercel_authorization: `Bearer ${vercelToken}` },
})
if (response.error != null) {
return { error: `Retrieve vercel projects failed. ${response.error?.message}` }
@@ -48,7 +48,7 @@ export async function fetchVercelProject({
url = `${url}?${query}`
}
const data = await get(url, {
headers: { Authorization: `Bearer ${vercelToken}` },
headers: { vercel_authorization: `Bearer ${vercelToken}` },
})
if (data.error != null) {
return { error: `Retrieve vercel project failed. ${data.error.message}` }
@@ -74,7 +74,7 @@ export async function fetchVercelProjectEnvs({
url = `${url}?${query}`
}
const response = await get(url, {
headers: { Authorization: `Bearer ${vercelToken}` },
headers: { vercel_authorization: `Bearer ${vercelToken}` },
})
if (response.error != null) {
return { error: `Retrieve vercel project envs failed. ${response.error?.message}` }
@@ -114,7 +114,7 @@ export async function createVercelEnv({
url,
{ key, value, type, target },
{
headers: { Authorization: `Bearer ${vercelToken}` },
headers: { vercel_authorization: `Bearer ${vercelToken}` },
}
)
}
+1 -1
View File
@@ -83,7 +83,7 @@ function checkRedirectTo(
redirectIfFound?: boolean
) {
if (loading) return false
if (router.asPath == redirectTo) return false
if (router.pathname == redirectTo) return false
// If redirectTo is set, redirect if the user was not found.
if (redirectTo && !redirectIfFound && !profile) return true
+42 -2
View File
@@ -1,6 +1,6 @@
import React from 'react'
import { observer } from 'mobx-react-lite'
import { IconMoon, IconSun, Select, Typography } from '@supabase/ui'
import { IconMoon, IconSun, Select, Typography, Input } from '@supabase/ui'
import { useProfile, useStore, withAuth } from 'hooks'
import { post } from 'lib/common/fetch'
@@ -32,8 +32,10 @@ export default withAuth(User)
const ProfileCard = observer(() => {
const { ui } = useStore()
const { mutateProfile } = useProfile()
const user = ui.profile
console.log('User', user?.username, user?.primary_email)
const updateUser = async (model: any) => {
try {
const updatedUser = await post(`${API_URL}/profile/update`, model)
@@ -51,6 +53,9 @@ const ProfileCard = observer(() => {
return (
<article className="p-4 max-w-4xl">
<section>
<GithubProfile />
</section>
<section className="">
{/* @ts-ignore */}
<SchemaFormPanel
@@ -77,6 +82,41 @@ const ProfileCard = observer(() => {
)
})
const GithubProfile = observer(() => {
const { ui } = useStore()
return (
<Panel
title={[
<Typography.Title key="panel-title" level={5} className="mb-0">
Account Information
</Typography.Title>,
]}
>
<Panel.Content>
<div className="space-y-2">
<Input
readOnly
disabled
label="Username"
layout="horizontal"
value=""
placeholder={ui.profile?.username ?? ''}
/>
<Input
readOnly
disabled
label="Email"
layout="horizontal"
value=""
placeholder={ui.profile?.primary_email ?? ''}
/>
</div>
</Panel.Content>
</Panel>
)
})
const ThemeSettings = observer(() => {
const { ui } = useStore()
+48 -4
View File
@@ -1,19 +1,21 @@
import { NextPage } from 'next'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { NextRouter, useRouter } from 'next/router'
import { isUndefined } from 'lodash'
import { Typography } from '@supabase/ui'
import { Project } from 'types'
import { useProfile, useStore, withAuth } from 'hooks'
import { post, delete_ } from 'lib/common/fetch'
import { API_URL, IS_PLATFORM, PROJECT_STATUS, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { API_URL, IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { AccountLayout } from 'components/layouts'
import { auth } from 'lib/gotrue'
import Landing from 'components/interfaces/Home/Landing'
import ProjectList from 'components/interfaces/Home/ProjectList'
import OrganizationDropdown from 'components/to-be-cleaned/Dropdown/OrganizationDropdown'
import TextConfirmModal from 'components/to-be-cleaned/ModalsDeprecated/TextConfirmModal'
import Connecting from 'components/ui/Loading'
const Home: NextPage = () => {
const { app, ui } = useStore()
@@ -26,7 +28,23 @@ const Home: NextPage = () => {
const [selectedProjectToDelete, setSelectedProjectToDelete] = useState<Project>()
if (!profile) {
return <Landing />
return <UnauthorizedLanding />
} else {
const isRedirect = isRedirectFromThirdPartyService(router)
if (isRedirect) {
const queryParams = (router.query as any) || {}
const params = new URLSearchParams(queryParams)
if (router.query?.next?.includes('https://vercel.com')) {
router.push(`/vercel/integrate?${params.toString()}`)
}
if (router.query?.next?.includes('new-project')) {
router.push('/new/project')
}
if (router.query['x-amzn-marketplace-token'] != undefined) {
router.push(`/account/associate?${params.toString()}`)
}
return <Connecting />
}
}
const onSelectDeleteProject = async (project: Project) => {
@@ -103,3 +121,29 @@ const Home: NextPage = () => {
)
}
export default withAuth(observer(Home))
// detect for redirect from 3rd party service like vercel, aws...
function isRedirectFromThirdPartyService(router: NextRouter) {
return router.query.next !== undefined || router.query['x-amzn-marketplace-token'] !== undefined
}
const UnauthorizedLanding = () => {
const router = useRouter()
const autoLogin = isRedirectFromThirdPartyService(router)
useEffect(() => {
if (autoLogin) {
const queryParams = (router.query as any) || {}
const params = new URLSearchParams(queryParams)
// trigger github signIn
auth.signIn(
{ provider: 'github' },
{
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}?${params.toString()}`,
}
)
}
}, [])
return autoLogin ? <Connecting /> : <Landing />
}
+5 -7
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, Typography } from '@supabase/ui'
import Router from 'next/router'
import { useRouter } from 'next/router'
import { API_URL } from 'lib/constants'
import { useStore, withAuth } from 'hooks'
@@ -15,6 +15,8 @@ import Panel from 'components/to-be-cleaned/Panel'
*/
const Wizard = () => {
const { ui, app } = useStore()
const router = useRouter()
const [orgName, setOrgName] = useState('')
const [newOrgLoading, setNewOrgLoading] = useState(false)
@@ -49,11 +51,7 @@ const Wizard = () => {
} else {
const org = response
app.onOrgAdded(org)
// Router.push('/new/[slug]', `/new/${org.slug}`)
// Using window.location.href for now as organization metadata needs to refresh
// We do not get is_owner nor stripe_customer_id from the response here
window.location.href = `/new/${org.slug}`
router.push(`/new/${org.slug}`)
}
}
@@ -70,7 +68,7 @@ const Wizard = () => {
]}
footer={[
<div key="panel-footer" className="flex items-center w-full justify-between">
<Button type="default" onClick={() => Router.push('/')}>
<Button type="default" onClick={() => router.push('/')}>
Cancel
</Button>
<div className="space-x-3">
@@ -188,6 +188,7 @@ const ResetDbPassword: FC<any> = () => {
} else {
ui.setNotification({ category: 'error', message: 'Failed to reset password' })
}
setIsUpdatingPassword(false)
}
}
@@ -279,9 +280,6 @@ const DownloadCertificate: FC<any> = ({ createdAt }) => {
}
const GeneralSettings: FC<any> = ({ projectRef }) => {
const { ui } = useStore()
const isOwner = ui.selectedOrganization?.is_owner
const { data, error }: any = useSWR(`${API_URL}/props/project/${projectRef}/settings`, get)
if (data?.error || error) {
@@ -378,7 +376,7 @@ const GeneralSettings: FC<any> = ({ projectRef }) => {
</Panel.Content>
</Panel>
</section>
{isOwner && <ResetDbPassword />}
<ResetDbPassword />
<DownloadCertificate createdAt={connectionInfo.inserted_at} />
</div>
<div>
+11 -1
View File
@@ -12,11 +12,13 @@ import {
} from '@supabase/ui'
import SVG from 'react-inlinesvg'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { API_URL } from 'lib/constants'
import { useStore, withAuth } from 'hooks'
import { post } from 'lib/common/fetch'
import { Project } from 'types'
import { isUndefined } from 'lodash'
const DEFAULT = {
category: {
@@ -49,6 +51,9 @@ function formReducer(state: any, action: any) {
const SupportNew = () => {
const { ui, app } = useStore()
const router = useRouter()
const projectRef = router.query.ref
const [formState, formDispatch] = useReducer(formReducer, DEFAULT)
const [errors, setErrors] = useState<any>([])
const [loading, setLoading] = useState<boolean>(false)
@@ -72,7 +77,12 @@ const SupportNew = () => {
useEffect(() => {
// set project default
if (sortedProjects.length > 1) {
handleOnChange({ name: 'project', value: sortedProjects[0].ref })
const selectedProject = sortedProjects.find((project: Project) => project.ref === projectRef)
if (!isUndefined(selectedProject)) {
handleOnChange({ name: 'project', value: selectedProject.ref })
} else {
handleOnChange({ name: 'project', value: sortedProjects[0].ref })
}
} else {
// set as 'No specific project'
handleOnChange({ name: 'project', value: projectDefaults[0].ref })
+1 -1
View File
@@ -226,7 +226,7 @@ const Connecting = () => (
const ProjectLinksEmptyState = () => (
<div className="flex flex-col space-y-4">
<Typography.Text>
You haven’t created a Supabase project yet. Get started by creating a new Supabase project,
You haven't created a Supabase project yet. Get started by creating a new Supabase project,
then close this window and retry adding integration.
</Typography.Text>
<Typography.Link href="https://app.supabase.io">
+11 -7
View File
@@ -1,6 +1,6 @@
import { cloneDeep } from 'lodash'
import { values } from 'mobx'
import { Project } from 'types'
import { Organization, Project } from 'types'
import { API_URL, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { IRootStore } from '../RootStore'
@@ -71,19 +71,23 @@ export default class AppStore implements IAppStore {
}
}
onOrgUpdated(org: any) {
if (org && org.id) {
this.organizations.data[org.id] = { ...org }
onOrgUpdated(updatedOrg: Organization) {
if (updatedOrg && updatedOrg.id) {
const originalOrg = this.organizations.data[updatedOrg.id]
this.organizations.data[updatedOrg.id] = {
...originalOrg,
...updatedOrg,
}
}
}
onOrgAdded(org: any) {
onOrgAdded(org: Organization) {
if (org && org.id) {
this.organizations.data[org.id] = { ...org }
this.organizations.data[org.id] = org
}
}
onOrgDeleted(org: any) {
onOrgDeleted(org: Organization) {
if (org && org.id) {
const projects = values(this.projects.data)
// cleanup projects saved queries
+6 -2
View File
@@ -4,9 +4,13 @@ export interface Organization {
name: string
billing_email: string
project_limit: number
stripe_customer_id?: string
total_free_projects?: number
members: any[]
projects: any[]
is_owner?: boolean
stripe_customer_id?: string
stripe_customer_object?: any
total_free_projects?: number
total_paid_projects?: number
}
export interface Project {
+2 -2
View File
@@ -1,5 +1,5 @@
process.env.SUPABASE_URL = 'http://localhost:8000'
process.env.SUPABASE_KEY_ANON =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYyNzIwODU0MCwiZXhwIjoxOTc0MzYzNzQwfQ.zcaQfHd3VA7XgJmdGfmV86OLVJT9s2MTmSy-e69BpUY'
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
process.env.SUPABASE_KEY_ADMIN =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjI3MjA4NTQwLCJleHAiOjE5NzQzNjM3NDB9.pkT3PNpO4DtO45Ac5HK_TKCx8sGLgNtV__pr_ZrRSAU'
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
+1 -3
View File
@@ -133,9 +133,7 @@ Migrating projects can be achieved using standard PostgreSQL tooling. This is pa
When you create a new project in Supabase we ask for a password. You can use this password to connect directly to your Postgres database.
If you forget your password, you can reset it from the Dashboard SQL editor:
For example:
If you forget your password, you can reset it from the Dashboard under the database settings page, or through the SQL editor as such:
```sql
alter user postgres
+42
View File
@@ -716,6 +716,48 @@ pages:
console.log(event, session)
})
```
- name: Listen to sign in
js: |
```js
supabase.auth.onAuthStateChange((event, session) => {
if (event == 'SIGNED_IN') console.log('SIGNED_IN', session)
})
```
- name: Listen to sign out
js: |
```js
supabase.auth.onAuthStateChange((event, session) => {
if (event == 'SIGNED_OUT') console.log('SIGNED_OUT', session)
})
```
- name: Listen to token refresh
js: |
```js
supabase.auth.onAuthStateChange((event, session) => {
if (event == 'TOKEN_REFRESHED') console.log('TOKEN_REFRESHED', session)
})
```
- name: Listen to user updates
js: |
```js
supabase.auth.onAuthStateChange((event, session) => {
if (event == 'USER_UPDATED') console.log('USER_UPDATED', session)
})
```
- name: Listen to user deleted
js: |
```js
supabase.auth.onAuthStateChange((event, session) => {
if (event == 'USER_DELETED') console.log('USER_DELETED', session)
})
```
- name: Listen to password recovery events
js: |
```js
supabase.auth.onAuthStateChange((event, session) => {
if (event == 'PASSWORD_RECOVERY') console.log('PASSWORD_RECOVERY', session)
})
```
auth.api.getUser():
title: 'getUser()'
$ref: '@supabase/gotrue-js.GoTrueApi.getUser'
+14 -10
View File
@@ -3,21 +3,23 @@ var axios = require('axios')
var fs = require('fs')
const query = `
query BIO_QUERY($username: String!) {
organization(login: $username) {
query Sponsors {
organization(login: "supabase") {
id
sponsorshipsAsMaintainer(first: 100) {
totalCount
sponsors(first: 100) {
nodes {
createdAt
privacyLevel
tier {
name
... on Organization {
id
createdAt
name : login
}
sponsor {
... on User {
id
createdAt
login
}
}
totalCount
}
}
}
@@ -39,10 +41,11 @@ const fetchAllSponsors = async () => {
},
}
)
return data.data.organization.sponsorshipsAsMaintainer
return data.data.organization.sponsors
}
const formatResults = (graphqlResponse) => {
console.log('graph', graphqlResponse)
return graphqlResponse.nodes
.filter((x) => !!x.sponsor)
.map((x) => ({
@@ -65,6 +68,7 @@ const writeFile = (data) => {
const main = async () => {
const sponsorsResponse = await fetchAllSponsors()
// console.log('sponsorsResponse.totalCount', sponsorsResponse.totalCount)
const formatted = formatResults(sponsorsResponse)
writeFile(formatted)
}
+1 -1
View File
@@ -199,4 +199,4 @@
"tier": "$49 a month",
"sponsor": "CryptoJobsList"
}
]
]
+1 -1
View File
@@ -253,7 +253,7 @@ const Nav = (props: Props) => {
<a href="https://app.supabase.io/">
<Button type="default">Sign in</Button>
</a>
<a href="https://app.supabase.io/">
<a href="https://app.supabase.io/new/project">
<Button>Start your project</Button>
</a>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
},
{
"question": "Is there a cap on the number of projects for the Free Tier?",
"answer": "Yes, on the Free Tier you can have a maximum of two organizations, each with up to two projects. This means that you can have a maximum of four free projects in your account."
"answer": "Yes, you have can up to 2 free projects in your account across all organizations that you own. You also are able to create as many organizations as you want."
},
{
"question": "When will I be billed?",