# Check auth endpoint status
Source: https://docs.basic.tech/api-reference/auth/check-auth-endpoint-status
get /auth/
# Get auth token
Source: https://docs.basic.tech/api-reference/auth/get-auth-token
post /auth/token
# Get user info
Source: https://docs.basic.tech/api-reference/auth/get-user-info
get /auth/userInfo
This endpoint is deprecated. Use /auth/userinfo instead.
# Redirect to sign in
Source: https://docs.basic.tech/api-reference/auth/redirect-to-sign-in
get /auth/authorize
# Create item
Source: https://docs.basic.tech/api-reference/pds/create-item-pds
post /account/{project_id}/db/{table}
# Update an item
Source: https://docs.basic.tech/api-reference/pds/delete-an-item-pds
delete /account/{project_id}/db/{table}/{item_id}
# Get all items
Source: https://docs.basic.tech/api-reference/pds/get-all-items-pds
get /account/{project_id}/db/{table}
# Get specific item in a table
Source: https://docs.basic.tech/api-reference/pds/get-specific-item-pds
get /account/{project_id}/db/{table}/{item_id}
# Replace an item
Source: https://docs.basic.tech/api-reference/pds/replace-an-item-pds
put /account/{project_id}/db/{table}/{item_id}
# Update an item
Source: https://docs.basic.tech/api-reference/pds/update-an-item-pds
patch /account/{project_id}/db/{table}/{item_id}
# Create new API key
Source: https://docs.basic.tech/api-reference/project-keys/create-new-api-key
post /project/{id}/key
Creates a new API key for the project with specified permissions
# Delete API key
Source: https://docs.basic.tech/api-reference/project-keys/delete-api-key
delete /project/{id}/key/{key_id}
Permanently deletes an API key from the project. This action cannot be undone.
# Get all keys for a project
Source: https://docs.basic.tech/api-reference/project-keys/get-all-keys
get /project/{id}/key
Retrieves all API keys associated with the project
# Get specific API key
Source: https://docs.basic.tech/api-reference/project-keys/get-specific-api-key
get /project/{id}/key/{key_id}
Retrieves details of a specific API key for the project
# Regenerate API key
Source: https://docs.basic.tech/api-reference/project-keys/regenerate-api-key
post /project/{id}/key/{key_id}/regenerate
Regenerates an existing API key with a new value while maintaining the same permissions and label
# Update API key
Source: https://docs.basic.tech/api-reference/project-keys/update-api-key
patch /project/{id}/key/{key_id}
Updates the label and/or roles of an existing API key
# Verify API key
Source: https://docs.basic.tech/api-reference/project-keys/verify-api-key
post /project/{id}/key/verify
Verifies if an API key is valid for the project and returns its associated permissions
# Create item
Source: https://docs.basic.tech/api-reference/project-users/create-item
post /project/{id}/user/{user_id}/db/{table_id}
# Delete a user from the project
Source: https://docs.basic.tech/api-reference/project-users/delete-a-user-from-the-project
delete /project/{id}/user/{user_id}
# Delete an item
Source: https://docs.basic.tech/api-reference/project-users/delete-an-item
delete /project/{id}/user/{user_id}/db/{table_id}/{item_id}
# Get all items in a table
Source: https://docs.basic.tech/api-reference/project-users/get-all-items
get /project/{id}/user/{user_id}/db/{table_id}
# Get all users in project
Source: https://docs.basic.tech/api-reference/project-users/get-project-users
get /project/{id}/user/
Retrieves all connected users for the project
# Get specific item in a table
Source: https://docs.basic.tech/api-reference/project-users/get-specific-item
get /project/{id}/user/{user_id}/db/{table_id}/{item_id}
# Get user details
Source: https://docs.basic.tech/api-reference/project-users/get-user-details
get /project/{id}/user/{user_id}
Retrieves details for a specific user, fetching profile from their PDS via service token
# Replace an item
Source: https://docs.basic.tech/api-reference/project-users/replace-an-item
put /project/{id}/user/{user_id}/db/{table_id}/{item_id}
# Update an item
Source: https://docs.basic.tech/api-reference/project-users/update-an-item-in-the-users-database-1
patch /project/{id}/user/{user_id}/db/{table_id}/{item_id}
# Update user metadata
Source: https://docs.basic.tech/api-reference/project-users/update-user-details
patch /project/{id}/user/{user_id}
# Check project slug availability
Source: https://docs.basic.tech/api-reference/projects/check-project-slug-availability
get /project/slug
Checks if a project slug is available for use
# Create a new project
Source: https://docs.basic.tech/api-reference/projects/create-a-new-project
post /project/
Creates a new project for the authenticated user and returns an API key for the project
# Delete a project
Source: https://docs.basic.tech/api-reference/projects/delete-a-project
delete /project/{id}
Permanently deletes a project and all its associated data. This action cannot be undone.
# Get all admin projects of developer
Source: https://docs.basic.tech/api-reference/projects/get-all-admin-projects
get /project/
Retrieves all projects owned by the authenticated user
# Get project details
Source: https://docs.basic.tech/api-reference/projects/get-project-details
get /project/{id}
Retrieves detailed information for a specific project
# Get project schema
Source: https://docs.basic.tech/api-reference/projects/get-project-schema
get /project/{id}/schema
Retrieves the schema configuration for a specific project. Can return either the latest version, a specific version, or all versions.
# Merge and update project schema
Source: https://docs.basic.tech/api-reference/projects/patch-project-schema
patch /project/{id}/schema
Merges the provided schema with the existing schema and creates a new version.
# Update project details
Source: https://docs.basic.tech/api-reference/projects/update-project-details
patch /project/{id}
Updates the details of a specific project. Only provided fields will be updated.
# Update project schema
Source: https://docs.basic.tech/api-reference/projects/update-project-schema
post /project/{id}/schema
Updates the schema configuration for a specific project. Creates a new version of the schema.
# Upload project image
Source: https://docs.basic.tech/api-reference/projects/upload-project-image
post /project/{id}/upload-image
# Accept a team invite
Source: https://docs.basic.tech/api-reference/teams/accept-a-team-invite
post /team/accept-invite
Accepts a team invitation and adds the user to the team
# Check team slug availability
Source: https://docs.basic.tech/api-reference/teams/check-team-slug-availability
get /team/slug
Checks if a team slug is available for use
# Create a new team
Source: https://docs.basic.tech/api-reference/teams/create-a-new-team
post /team/
Creates a new team for the authenticated user
# Create a team invite
Source: https://docs.basic.tech/api-reference/teams/create-a-team-invite
post /team/{team_id}/invite
Creates a new invite for a team
# Delete a team invite
Source: https://docs.basic.tech/api-reference/teams/delete-a-team-invite
delete /team/{team_id}/invite/{invite_id}
Deletes a team invitation
# Delete a team member
Source: https://docs.basic.tech/api-reference/teams/delete-a-team-member
delete /team/{team_id}/member/{member_id}
Removes a member from a team
# Get team by ID
Source: https://docs.basic.tech/api-reference/teams/get-team-by-id
get /team/{team_id}
Retrieves a specific team by its ID
# Get team invites
Source: https://docs.basic.tech/api-reference/teams/get-team-invites
get /team/{team_id}/invite
Retrieves all pending invites for a specific team
# Get team members
Source: https://docs.basic.tech/api-reference/teams/get-team-members
get /team/{team_id}/member
Retrieves all members of a team
# Get user teams
Source: https://docs.basic.tech/api-reference/teams/get-user-teams
get /team/
Retrieves all teams the user is a member of
# Update a team
Source: https://docs.basic.tech/api-reference/teams/update-a-team
patch /team/{team_id}
Updates a team's name and/or slug
# Update a team member
Source: https://docs.basic.tech/api-reference/teams/update-a-team-member
patch /team/{team_id}/member/{member_id}
Updates a team member's roles or role_name
# Check utils endpoint status
Source: https://docs.basic.tech/api-reference/utils/check-utils-endpoint-status
get /utils/
# Compare two schemas
Source: https://docs.basic.tech/api-reference/utils/compare-two-schemas
post /utils/schema/compareSchema
Compare two schemas and return the differences
# Convert text to URL-friendly slug
Source: https://docs.basic.tech/api-reference/utils/convert-text-to-url-friendly-slug
get /utils/slugify
Converts any text into a URL-friendly slug by removing special characters, converting to lowercase, and replacing spaces with hyphens
# Verify update schema
Source: https://docs.basic.tech/api-reference/utils/verify-update-schema
post /utils/schema/verifyUpdateSchema
Verify update schema
# ⚡️ Quickstart
Source: https://docs.basic.tech/basic-expo-rn/basic-rn-sdk
Using the @basictech/expo library with your React Native project
## Installation
Install the package via npm:
```bash theme={null}
npm install @basictech/expo
```
## Define a schema
Define your database schema as a TypeScript constant (e.g., `src/schema.ts`):
```typescript theme={null}
// src/basic.config.ts
export const schema = {
project_id: 'YOUR_PROJECT_ID', // Replace with your actual project ID
version: 1,
tables: {
// example table for a notes app
notes: {
type: 'collection',
fields: {
title: { type: 'string', indexed: true },
content: { type: 'string' },
createdAt: { type: 'number', indexed: true },
completed: { type: 'boolean', indexed: true },
priority: { type: 'number', indexed: true },
tags: { type: 'json', indexed: true }
},
},
// Add other tables here
},
};
```
## Provider Integration
Wrap your application's root with the `BasicProvider`:
```tsx theme={null}
// App.tsx
import { BasicProvider } from '@basictech/expo';
import { schema } from './basic.config';
import MainApp from './MainApp';
export default function App() {
return (
);
}
```
`project_id` is inferred from `schema.project_id` on `BasicProvider`; passing `project_id` separately is deprecated.
**Note:** Set the correct URI scheme under `expo.scheme` in `app.json` so OAuth redirects return to your app.
## Authentication & State Management
Use the `useBasic` hook from `@basictech/expo` inside your components to access authentication state and database client:
```tsx theme={null}
import { useBasic } from '@basictech/expo';
const { user, signIn, signOut, db, isLoading, isSignedIn } = useBasic();
```
**Returned properties:**
* `user`: The authenticated user or null.
* `signIn()`: Starts the OAuth login flow.
* `signOut()`: Logs the user out, clearing all tokens.
* `db`: The main [database client](/sdk-reference/expo-rn).
* `isLoading`: True during authentication state checks.
* `isSignedIn`: Whether a user is authenticated.
# Set up your Basic account
Source: https://docs.basic.tech/basic-expo-rn/rn-adminportal
Using the admin portal to create a new project.
# Create your project
Head to [https://app.basic.tech](https://app.basic.tech) to begin creating your project.
Login with your Basic account. If you don't have one yet, signup to Basic.
After creating your Basic account, head to the Dashboard and create a new project.
Use the form to create a project with a unique name and slug. Make sure not to leave the name or slug fields blank.
This page allows you to view all your projects and manage them. To view project details, click into any of them.
You'll need these if you're using the API to connect to your database. Store them securely.
Keep your API Key a secret! Only use it on a secure server environment.
Make sure to replace `` and ``.
```js GET Project Details theme={null}
fetch("https://api.basic.tech/project/", {
headers: {
"Authorization": "Bearer ",
},
}).then(response => response.json())
.then(data => console.log(data));
```
# Create your schema
There are two ways to create your schema, either using the table UI, or by using the code editor. Let's explore the table UI first.
Give your table a unique name, and add fields to it. Fields need to have a unique name within each table, and you can use the dropdown to select the type of each field.
Fields can take on the following types: `string`, `number`, `boolean`, `json`.
You can also edit your schema with the code editor.
Once you've added your tables, you can publish your changes. Upon publishing, you'll notice your version number has been automatically incremented.
You may also discard changes if you'd like to revert to the existing version.
# Next steps
You can now start building your app!
If you are using a React app, we recommend you use the [React SDK](/basic-react/basic-react-sdk) to get started.
Otherwise, you can use the [API](/basic-restapi/basic-api) for any language.
# Filtering, ordering, pagination
Source: https://docs.basic.tech/basic-expo-rn/rn-filtering
Learn how to filter results using the Expo / RN SDK
## Querying
Chain after `.getAll()`:
### Filtering (`filter`)
```js theme={null}
const notes = await db.from('notes').getAll().filter({ completed: true });
```
* **Method:** `filter(conditions)`
* **Supported Operators:** eq, neq, gt, gte, lt, lte, like, ilike, in, not (see [list of operators](#supported-operators))
### Ordering (`order`)
```js theme={null}
const ordered = await db.from('notes').getAll().order('created_at', 'desc');
```
* **Method:** `order(field, direction?)` (`direction` defaults to 'asc')
### Pagination (`limit`, `offset`)
```js theme={null}
const paged = await db.from('notes').getAll().limit(10).offset(10);
```
* **Methods:** `limit(count)`, `offset(count)`
**Note:** All these must be chained after `.getAll()`.
## Supported operators
All used within `filter()`:
| Operator | Description | Example |
| -------- | -------------------------- | -------------------------------------------------- |
| `eq` | Equality | `.filter({ status: { eq: "active" } })` |
| `neq` | Not equal | `.filter({ status: { neq: "deleted" } })` |
| `gt` | Greater than | `.filter({ priority: { gt: 3 } })` |
| `gte` | Greater than or equal | `.filter({ priority: { gte: 3 } })` |
| `lt` | Less than | `.filter({ priority: { lt: 3 } })` |
| `lte` | Less than or equal | `.filter({ priority: { lte: 3 } })` |
| `like` | Pattern (case sensitive) | `.filter({ title: { like: "%project%" } })` |
| `ilike` | Pattern (case insensitive) | `.filter({ title: { ilike: "%todo%" } })` |
| `in` | Value in set | `.filter({ tags: { in: ["work", "important"] } })` |
| `not` | Negation | `.filter({ tags: { not: { eq: "work" } } })` |
## Limitations
* Only one filterable condition per field per query (no compound filters across multiple fields)
* Range filters (e.g., `{ gte: x, lte: y }`) not supported
* Method chaining order is flexible, but all must be chained after `getAll()`
# Schema
Source: https://docs.basic.tech/basic-expo-rn/rn-schema
Deeper dive into how schemas work in Basic
# Overview
In Basic, we declare schemas in a JSON format as part of the `basic.config.ts` or `basic.config.js` file that lives in your repository.
These `basic.config` files can be committed to git, and unlock "schema-as-code" capabilities for your project.
Basic schemas are very similar to SQL/Postgres tables with the following differences:
* no separate users table required (Basic's datastore per user model already ties data to each specific user)
* no need to declare RLS
* `id` and `created_at` fields are created automatically for you
Schemas would follow this general format:
The top level requires three parameters:
### project\_id
This is your project ID, which you can find in the Basic dashboard, or will be auto-generated for you if you had used the [CLI to create your project](/get-started/cli).
### version
This will be used for versioning your schema. It should be an integer that starts at 0 and increments by 1 for each new version.
# Tables
Let's double click into the tables to learn how they are declared in the schema.
Each table is declared as an object where the key is the table name and the value is an object with the following two parameters:
### type
This refers to the type of database table you are creating. At the moment, Basic only supports `collection`, which will operate similarly to MongoDB collections.
### fields
Fields are the columns of the table. They will be declared as an object of objects. Learn more about the fields below.
# Fields
Each field is declared as an object where the key is the field name and the value is an object with the following 2 parameters:
Default values for fields are not supported yet. Nesting fields is not supported; please use a flat structure for field declarations.
### type
This is the data type of the field. Basic supports the following field types: `string`, `number`, `boolean`, `json`.
### indexed
This lets you control if the field should be indexed in the user's client database for local-first benefits. For now, this should always be set to `true`.
# Example
When put all together, your schema could look like this:
```javascript basic.config.ts theme={null}
export const schema = {
project_id: '1234567890',
version: 0,
tables: {
comments: {
type: 'collection',
fields: {
title: {
type: 'string',
indexed: true
},
body: {
type: 'string',
indexed: true
},
rating: {
type: 'number',
indexed: true
}
}
},
items: {
type: 'collection',
fields: {
name: {
type: 'string',
indexed: true
},
quantity: {
type: 'number',
indexed: true
},
available: {
type: 'boolean',
indexed: true
}
}
}
}
}
```
# Quickstart: Next.js
Source: https://docs.basic.tech/basic-nextjs/basic-nextjs-sdk
Using the @basictech/nextjs library with your Next.js project
In this guide, you will:
1. Install the Basic Next.js SDK into an existing Next.js project
2. Connect to a Basic project and customize the schema
3. Add sign in and sign out functionality
This library is for Next.js applications using the App Router. For plain React apps, see the [@basictech/react documentation](/basic-react/basic-react-sdk).
# Installation
Navigate to your project and install the Basic Next.js SDK:
```bash Terminal theme={null}
cd your-nextjs-project
npm install @basictech/nextjs
```
# Set up basic.config
Create a new file called `basic.config.ts` in the root of your project:
```ts basic.config.ts theme={null}
export const schema = {
project_id: 'YOUR_PROJECT_ID',
version: 0,
tables: {
todos: {
type: 'collection',
fields: {
title: {
type: 'string',
indexed: true,
},
completed: {
type: 'boolean',
}
}
}
}
}
```
Replace `YOUR_PROJECT_ID` with your Project ID from the [Basic dashboard](https://admin.basic.tech).
# Create a Client Provider
Create `app/providers.tsx` to wrap your app with BasicProvider:
```tsx app/providers.tsx theme={null}
'use client'
import { BasicProvider } from '@basictech/nextjs/client'
import { schema } from '../basic.config'
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
The `'use client'` directive is required because BasicProvider uses React context and hooks.
Update `app/layout.tsx` to use the providers:
```tsx app/layout.tsx theme={null}
import { Providers } from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
{children}
)
}
```
# Adding Authentication
Create a component to handle authentication. This must be a client component:
```tsx app/components/AuthButton.tsx theme={null}
'use client'
import { useBasic } from '@basictech/nextjs/client'
export function AuthButton() {
const { isReady, isSignedIn, user, signIn, signOut } = useBasic()
if (!isReady) {
return
Loading...
}
if (!isSignedIn) {
return
}
return (
Signed in as: {user?.email}
)
}
```
Add the auth component to your page:
```tsx app/page.tsx theme={null}
import { AuthButton } from './components/AuthButton'
export default function Home() {
return (
My App
)
}
```
Clicking the sign in button will redirect to the Basic.id login page. After signing in, users are redirected back to your app.
***
# Important: Import Patterns
Due to Next.js SSR, you must use the correct import paths to avoid "self is not defined" errors.
| File Type | Import From |
| ---------------------------------- | ------------------------------------------------ |
| Client components (`'use client'`) | `@basictech/nextjs/client` or `@basictech/react` |
| Server components / middleware | `@basictech/nextjs` |
```tsx theme={null}
// ✅ Correct - in a client component
'use client'
import { useBasic, BasicProvider } from '@basictech/nextjs/client'
// ✅ Also correct - importing directly from react
'use client'
import { useBasic, BasicProvider } from '@basictech/react'
// ❌ Wrong - will cause SSR errors
import { useBasic } from '@basictech/nextjs' // Don't do this in client components!
```
***
# Using the Database
Once authenticated, you can use the database in your client components:
```tsx app/components/TodoList.tsx theme={null}
'use client'
import { useBasic, useQuery } from '@basictech/nextjs/client'
export function TodoList() {
const { db, isSignedIn } = useBasic()
// Subscribe to todos - automatically updates when data changes
const todos = useQuery(() => db.collection('todos').getAll())
const addTodo = async () => {
await db.collection('todos').add({
title: 'New todo',
completed: false
})
}
if (!isSignedIn) {
return
Please sign in to view todos
}
return (
{todos?.map((todo) => (
{todo.title}
))}
)
}
```
***
# Next Steps
* [Database Modes](/basic-nextjs/nextjs-modes) - Learn about sync vs remote mode
* [Middleware](/basic-nextjs/nextjs-middleware) - Protect routes with authentication
* [Database Methods](/sdk-reference/react-db) - Full database API reference
# Middleware: Route Protection
Source: https://docs.basic.tech/basic-nextjs/nextjs-middleware
Protect routes with authentication middleware in Next.js
The `@basictech/nextjs` package includes middleware utilities to protect routes and require authentication before accessing certain pages.
# Quick Setup
Create a `middleware.ts` file in your project root:
```ts middleware.ts theme={null}
import { createBasicMiddleware } from '@basictech/nextjs'
export const middleware = createBasicMiddleware({
protectedRoutes: ['/dashboard/*', '/settings/*', '/api/protected/*'],
publicRoutes: ['/login', '/signup', '/'],
signInUrl: '/login'
})
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}
```
With this setup:
* Routes matching `/dashboard/*`, `/settings/*`, or `/api/protected/*` require authentication
* Unauthenticated users are redirected to `/login`
* The return URL is preserved so users can be redirected back after signing in
***
# Configuration Options
## createBasicMiddleware
The main function to create authentication middleware.
```ts theme={null}
createBasicMiddleware(config?: BasicMiddlewareConfig)
```
### Options
| Option | Type | Default | Description |
| --------------------- | ---------- | ---------------------------------- | ------------------------------------------------------------------------------ |
| `protectedRoutes` | `string[]` | `[]` | Routes that require authentication. Supports glob patterns like `/dashboard/*` |
| `publicRoutes` | `string[]` | `['/login', '/signup', '/auth/*']` | Routes that are always public (bypass auth check) |
| `signInUrl` | `string` | `'/login'` | Where to redirect unauthenticated users |
| `afterSignInUrl` | `string` | `'/'` | Where to redirect after successful sign-in |
| `tokenCookieName` | `string` | `'basic_access_token'` | Cookie name for the access token |
| `fullTokenCookieName` | `string` | `'basic_token'` | Cookie name for the full token object |
### Route Matching
Routes support simple glob patterns with `*` wildcard:
```ts theme={null}
{
protectedRoutes: [
'/dashboard/*', // Matches /dashboard, /dashboard/settings, etc.
'/api/user/*', // Matches /api/user, /api/user/profile, etc.
'/admin', // Matches exactly /admin
],
publicRoutes: [
'/auth/*', // Matches /auth/callback, /auth/error, etc.
'/', // Matches exactly /
]
}
```
***
# Alternative: Simple Auth Middleware
For simpler use cases, use `withBasicAuth` which applies default settings:
```ts middleware.ts theme={null}
import { withBasicAuth } from '@basictech/nextjs'
export const middleware = withBasicAuth
export const config = {
// Only run middleware on these routes
matcher: ['/dashboard/:path*', '/settings/:path*']
}
```
This redirects unauthenticated users to `/login` for any matched routes.
***
# Utility Functions
## getAuthFromRequest
Check authentication status from a request:
```ts theme={null}
import { getAuthFromRequest } from '@basictech/nextjs'
export function middleware(request: NextRequest) {
const { isAuthenticated, token } = getAuthFromRequest(request)
if (!isAuthenticated) {
// Handle unauthenticated request
}
// Use token for API calls if needed
}
```
## getReturnUrl
Get the return URL from search params (useful on login pages):
```ts theme={null}
import { getReturnUrl } from '@basictech/nextjs'
export function middleware(request: NextRequest) {
const returnUrl = getReturnUrl(request, '/')
// After sign-in, redirect to returnUrl
}
```
***
# Full Example
```ts middleware.ts theme={null}
import { createBasicMiddleware } from '@basictech/nextjs'
export const middleware = createBasicMiddleware({
// Protect dashboard and API routes
protectedRoutes: [
'/dashboard/*',
'/settings/*',
'/api/user/*',
'/api/protected/*'
],
// Keep these public
publicRoutes: [
'/',
'/login',
'/signup',
'/about',
'/api/public/*',
'/auth/*'
],
// Custom sign-in page
signInUrl: '/login',
// Redirect here after signing in (if no return URL)
afterSignInUrl: '/dashboard'
})
// Run middleware on all routes except static files
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\..*).*)']
}
```
***
# Login Page Example
Create a login page that handles the return URL:
```tsx app/login/page.tsx theme={null}
'use client'
import { useBasic } from '@basictech/nextjs/client'
import { useSearchParams, useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function LoginPage() {
const { isReady, isSignedIn, signIn } = useBasic()
const searchParams = useSearchParams()
const router = useRouter()
const returnUrl = searchParams.get('returnUrl') || '/dashboard'
useEffect(() => {
// If already signed in, redirect to return URL
if (isReady && isSignedIn) {
router.push(returnUrl)
}
}, [isReady, isSignedIn, returnUrl, router])
if (!isReady) {
return
Loading...
}
return (
Sign In
Sign in to access your dashboard
)
}
```
# Database Modes: Sync vs Remote
Source: https://docs.basic.tech/basic-nextjs/nextjs-modes
Understanding the two database modes in the Basic SDK
The Basic SDK supports two database modes that determine how data is stored and synchronized. Choose the mode that best fits your application's needs.
# Overview
| Feature | Sync Mode (Default) | Remote Mode |
| ----------------------- | ------------------- | ------------------ |
| Local Storage | IndexedDB | None |
| Offline Support | Yes | No |
| Real-time Updates | Yes (WebSocket) | No |
| `useQuery` Auto-refresh | Yes | No |
| SSR Compatible | Partial | Yes |
| Authentication Required | For sync only | For all operations |
***
# Sync Mode (Default)
Sync mode provides a local-first experience with automatic synchronization to the cloud.
```tsx app/providers.tsx theme={null}
'use client'
import { BasicProvider } from '@basictech/nextjs/client'
import { schema } from '../basic.config'
export function Providers({ children }: { children: React.ReactNode }) {
return (
// dbMode defaults to 'sync'
{children}
)
}
```
## How It Works
1. Data is stored locally in IndexedDB
2. Changes are immediately available locally (instant UI updates)
3. A WebSocket connection syncs changes to/from the cloud
4. Other devices and users receive updates in real-time
## Benefits
* **Works Offline**: Users can read and write data without an internet connection. Changes sync when they reconnect.
* **Instant UI**: No loading spinners for data operations—changes appear immediately.
* **Real-time Sync**: All connected clients see changes instantly via WebSocket.
* **`useQuery` Auto-refresh**: Components using `useQuery` automatically re-render when data changes.
## Considerations
* Requires IndexedDB (browser environment)
* Initial sync downloads all user data
* Not suitable for server-side rendering of user data
## Example Usage
```tsx theme={null}
'use client'
import { useBasic, useQuery } from '@basictech/nextjs/client'
export function TodoList() {
const { db } = useBasic()
// useQuery subscribes to changes - automatically re-renders
const todos = useQuery(() => db.collection('todos').getAll())
const addTodo = async () => {
// Appears instantly in the UI, syncs to cloud in background
await db.collection('todos').add({ title: 'New todo', completed: false })
}
return (
{todos?.map(todo =>
{todo.title}
)}
)
}
```
***
# Remote Mode
Remote mode makes direct REST API calls to the server without local storage.
```tsx app/providers.tsx theme={null}
'use client'
import { BasicProvider } from '@basictech/nextjs/client'
import { schema } from '../basic.config'
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
## How It Works
1. Every database operation makes an HTTP request to the Basic API
2. No local caching or storage
3. Data is fetched fresh on each request
## Benefits
* **No IndexedDB Dependencies**: Works in environments where IndexedDB isn't available
* **SSR-Friendly**: Better for server-side rendering patterns
* **Simple Architecture**: No sync complexity, just API calls
* **Always Fresh**: Data is always fetched from the server
## Considerations
* **Requires Internet**: No offline support
* **Requires Authentication**: All operations (including reads) require the user to be signed in
* **No Real-time Updates**: `useQuery` won't auto-update; you need to manually refetch
* **Network Latency**: Each operation waits for the network round-trip
## Example Usage
```tsx theme={null}
'use client'
import { useBasic } from '@basictech/nextjs/client'
import { useState, useEffect } from 'react'
export function TodoList() {
const { db, isSignedIn } = useBasic()
const [todos, setTodos] = useState([])
const [loading, setLoading] = useState(true)
// In remote mode, manually fetch and manage state
useEffect(() => {
if (isSignedIn) {
loadTodos()
}
}, [isSignedIn])
const loadTodos = async () => {
setLoading(true)
const data = await db.collection('todos').getAll()
setTodos(data)
setLoading(false)
}
const addTodo = async () => {
await db.collection('todos').add({ title: 'New todo', completed: false })
// Manually refetch after mutation
await loadTodos()
}
if (!isSignedIn) return
Please sign in
if (loading) return
Loading...
return (
{todos.map(todo =>
{todo.title}
)}
)
}
```
***
# Error Handling in Remote Mode
Remote mode throws `NotAuthenticatedError` when attempting write operations without being signed in:
```tsx theme={null}
import { NotAuthenticatedError } from '@basictech/nextjs/client'
const addTodo = async () => {
try {
await db.collection('todos').add({ title: 'New todo' })
} catch (error) {
if (error instanceof NotAuthenticatedError) {
// Prompt user to sign in
console.log('Please sign in to add todos')
} else {
throw error
}
}
}
```
## Graceful Degradation for Reads
In remote mode, read operations gracefully handle unauthenticated state:
| Operation | Unauthenticated Behavior |
| ---------- | ------------------------------ |
| `getAll()` | Returns empty array `[]` |
| `get(id)` | Returns `null` |
| `filter()` | Returns empty array `[]` |
| `add()` | Throws `NotAuthenticatedError` |
| `put()` | Throws `NotAuthenticatedError` |
| `update()` | Throws `NotAuthenticatedError` |
| `delete()` | Throws `NotAuthenticatedError` |
***
# When to Use Each Mode
## Use Sync Mode When:
* Building a PWA or offline-capable app
* You want real-time collaboration features
* Instant UI feedback is important
* Users work with moderate amounts of data
## Use Remote Mode When:
* Building an SSR-heavy Next.js application
* IndexedDB isn't available in your environment
* You don't need offline support
* You want simple, stateless API calls
* Data is too large to sync locally
***
# Checking Current Mode
You can check which mode is active using `dbMode` from `useBasic`:
```tsx theme={null}
'use client'
import { useBasic } from '@basictech/nextjs/client'
export function DbStatus() {
const { dbMode, dbStatus } = useBasic()
return (
Mode: {dbMode}
Status: {dbStatus}
)
}
```
# ⚡️ Quickstart existing React app
Source: https://docs.basic.tech/basic-react/basic-react-sdk
Using the @basictech/react library with your React project
In this guide, you will:
1. Install the Basic React SDK into an existing React project
2. Connect to a Basic project and customize the schema
3. Add sign in and sign out functionality
This library is for client-side React. For Next.js applications, see the [@basictech/nextjs documentation](/basic-nextjs/basic-nextjs-sdk).
# Installation
`cd project-name` into your project, run `npm i` and install the `@basictech/react` library in your app:
```bash Terminal theme={null}
# first, cd into your project
cd project-name
# then, install all packages
npm i
# finally, install Basic
npm install @basictech/react
```
# Set up basic.config
Create a new file called `basic.config.ts` in the root of your project.
Add your project ID to the `basic.config.ts` file, and customize the schema in the file. You can copy your schema from the [admin console](https://admin.basic.tech) (see [Admin & project setup](/get-started/adminportal#create-your-schema)) and paste it into the file.
Any changes you make to the schema in `basic.config` should eventually match the schema published for your project in the admin console. Local edits are for development until you deploy or sync the canonical schema.
```ts basic.config.ts theme={null}
export const schema = {
project_id: 'YOUR_PROJECT_ID',
version: 0,
tables: {
table_name: {
type: 'collection',
fields: {
field_name: {
type: 'string',
indexed: true,
}
}
}
}
}
```
# Adding Auth
Navigate to `src/main.tsx`, add the BasicProvider to imports, and wrap your app in it.
Replace `YOUR_PROJECT_ID` with your project ID from [admin.basic.tech](https://admin.basic.tech). New to Basic? Follow [Admin & project setup](/get-started/adminportal).
```tsx main.tsx theme={null}
// add the BasicProvider to your existing imports
import { BasicProvider } from '@basictech/react'
import { schema } from '../basic.config'
createRoot(document.getElementById('root')).render(
{/* Wrap your app in BasicProvider with your schema */}
,
)
```
Now navigate to `src/App.tsx` and import `signIn`, `isSignedIn`, and `user`. Vite comes with some template code, but you can replace the contents with the following:
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
import './App.css'
function App() {
// Import signIn to enable the user to sign in
// Import isSignedIn to check if the user is signed in
// Import user to get the user's information once they're signed in
const { signIn, isSignedIn, user } = useBasic()
return (
<>
{/* if the user isn't signed in, show the sign in button */}
{!isSignedIn ? (
) : (
// once signed in, show their email
Signed in as: {user?.email}
)}
>
)
}
export default App
```
Let's test it out! Clicking on the button should now redirect you to the [Basic.id](http://Basic.id) page where your users can login or create an account.
Once they're done, they'll be redirected back to your app and see their email displayed.
Use the isSignedIn hook to check if the user is logged in, and the signOut function to sign out the user:
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
import './App.css'
function App() {
// Add signOut to the imports
const { signIn, isSignedIn, user, signOut } = useBasic()
return (
<>
{isSignedIn ? (
Signed in as: {user?.email}
{/* Add a button to sign out */}
) : (
)}
>
)
}
export default App
```
That's it! You've successfully added Basic Auth to your React app 🎉
***
# Using the database
You can use the React SDK to read and write to the user's database. Checkout the [React SDK docs](/sdk-reference/react-components) to get started.
# Set up your Basic account
Source: https://docs.basic.tech/basic-react/react-adminportal
Using the admin portal to create a new project.
# Create your project
Head to [https://app.basic.tech](https://app.basic.tech) to begin creating your project.
Login with your Basic account. If you don't have one yet, signup to Basic.
After creating your Basic account, head to the Dashboard and create a new project.
Use the form to create a project with a unique name and slug. Make sure not to leave the name or slug fields blank.
This page allows you to view all your projects and manage them. To view project details, click into any of them.
You'll need these if you're using the API to connect to your database. Store them securely.
Keep your API Key a secret! Only use it on a secure server environment.
Make sure to replace `` and ``.
```js GET Project Details theme={null}
fetch("https://api.basic.tech/project/", {
headers: {
"Authorization": "Bearer ",
},
}).then(response => response.json())
.then(data => console.log(data));
```
# Create your schema
There are two ways to create your schema, either using the table UI, or by using the code editor. Let's explore the table UI first.
Give your table a unique name, and add fields to it. Fields need to have a unique name within each table, and you can use the dropdown to select the type of each field.
Fields can take on the following types: `string`, `number`, `boolean`, `json`.
You can also edit your schema with the code editor.
Once you've added your tables, you can publish your changes. Upon publishing, you'll notice your version number has been automatically incremented.
You may also discard changes if you'd like to revert to the existing version.
# Next steps
You can now start building your app!
If you are using a React app, we recommend you use the [React SDK](/basic-react/basic-react-sdk) to get started.
Otherwise, you can use the [API](/basic-restapi/basic-api) for any language.
# Schema
Source: https://docs.basic.tech/basic-react/react-schema
Deeper dive into how schemas work in Basic
# Overview
In Basic, we declare schemas in a JSON format as part of the `basic.config.ts` or `basic.config.js` file that lives in your repository.
These `basic.config` files can be committed to git, and unlock "schema-as-code" capabilities for your project.
Basic schemas are very similar to SQL/Postgres tables with the following differences:
* no separate users table required (Basic's datastore per user model already ties data to each specific user)
* no need to declare RLS
* `id` and `created_at` fields are created automatically for you
Schemas would follow this general format:
The top level requires three parameters:
### project\_id
This is your project ID, which you can find in the Basic dashboard, or will be auto-generated for you if you had used the [CLI to create your project](/get-started/cli).
### version
This will be used for versioning your schema. It should be an integer that starts at 0 and increments by 1 for each new version.
# Tables
Let's double click into the tables to learn how they are declared in the schema.
Each table is declared as an object where the key is the table name and the value is an object with the following two parameters:
### type
This refers to the type of database table you are creating. At the moment, Basic only supports `collection`, which will operate similarly to MongoDB collections.
### fields
Fields are the columns of the table. They will be declared as an object of objects. Learn more about the fields below.
# Fields
Each field is declared as an object where the key is the field name and the value is an object with the following 2 parameters:
Default values for fields are not supported yet. Nesting fields is not supported; please use a flat structure for field declarations.
### type
This is the data type of the field. Basic supports the following field types: `string`, `number`, `boolean`, `json`.
### indexed
This lets you control if the field should be indexed in the user's client database for local-first benefits. For now, this should always be set to `true`.
# Example
When put all together, your schema could look like this:
```javascript basic.config.ts theme={null}
export const schema = {
project_id: '1234567890',
version: 0,
tables: {
comments: {
type: 'collection',
fields: {
title: {
type: 'string',
indexed: true
},
body: {
type: 'string',
indexed: true
},
rating: {
type: 'number',
indexed: true
}
}
},
items: {
type: 'collection',
fields: {
name: {
type: 'string',
indexed: true
},
quantity: {
type: 'number',
indexed: true
},
available: {
type: 'boolean',
indexed: true
}
}
}
}
}
```
# Full Guide: Build a to-do app
Source: https://docs.basic.tech/basic-react/to-do-app
A step-by-step guide to building a simple to-do app using Basic
In this guide, you will:
1. Set up a new React project with Basic
2. Create a simple To-Do UI
3. Connect to a Basic project and set up your schema
4. Add sign in and sign out functionality
5. Hook up to Basic DB so that your users can save their to-dos across their devices
## Create a new React project
```bash Terminal theme={null}
npm create vite@latest my-todo-app -- --template react-ts
cd my-todo-app
npm install
```
```bash Terminal theme={null}
npm install @basictech/react
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
Update `tailwind.config.js`:
```js tailwind.config.js theme={null}
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
```
Replace `src/index.css` with:
```css src/index.css theme={null}
@tailwind base;
@tailwind components;
@tailwind utilities;
```
```bash Terminal theme={null}
npm run dev
```
Navigate to `http://localhost:5173` to see your app running.
## Create a simple To-Do UI
Replace the code in your `src/App.tsx` file:
```tsx src/App.tsx theme={null}
import './App.css'
function App() {
return (
my lofi to-do app
)
}
export default App
```
## Connect to a Basic project
Go to [admin.basic.tech](https://admin.basic.tech) and create a free account. Then create a new project and copy your Project ID.
Create a `basic.config.ts` file in your project root:
```ts basic.config.ts theme={null}
export const schema = {
project_id: "YOUR_PROJECT_ID", // Replace with your project ID
version: 0,
tables: {
todos: {
type: "collection",
fields: {
name: {
type: "string"
},
completed: {
type: "boolean"
}
}
}
}
}
```
Update `src/main.tsx`:
```tsx src/main.tsx theme={null}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BasicProvider } from '@basictech/react'
import { schema } from '../basic.config'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
,
)
```
## Add sign in and sign out functionality
Update `src/App.tsx`:
```tsx src/App.tsx theme={null}
import { useBasic } from '@basictech/react'
import './App.css'
function App() {
const { signIn, signOut, isSignedIn, user } = useBasic()
return (
my lofi to-do app
{isSignedIn ? (
Signed in as: {user?.email}
) : (
)}
)
}
export default App
```
## Hook up to Basic DB
Update `src/App.tsx` to add state and the `addTask` function:
```tsx src/App.tsx theme={null}
import { useBasic } from '@basictech/react'
import { useState } from 'react'
import './App.css'
function App() {
const { signIn, signOut, isSignedIn, user, db } = useBasic()
const [taskInput, setTaskInput] = useState('')
const addTask = async () => {
if (!taskInput.trim()) return
await db.collection('todos').add({
name: taskInput,
completed: false
})
setTaskInput('')
}
return (
)
}
export default App
```
And that's it! You've built a local-first to-do app that allows your users to sign in and save their to-dos across their devices.
Notice how snappily all the changes are reflected - from task creation to deletion. This is the power of local-first architecture!
## Next steps
* [Database Methods](/sdk-reference/react-db) - Learn about all available database operations
* [React Hooks](/sdk-reference/react-hooks) - Explore all available hooks
* [Schema Guide](/basic-react/react-schema) - Learn how to customize your schema
# Set up your Basic account
Source: https://docs.basic.tech/basic-restapi/api-adminportal
Using the admin portal to create a new project.
# Create your project
Head to [https://app.basic.tech](https://app.basic.tech) to begin creating your project.
Login with your Basic account. If you don't have one yet, signup to Basic.
After creating your Basic account, head to the Dashboard and create a new project.
Use the form to create a project with a unique name and slug. Make sure not to leave the name or slug fields blank.
This page allows you to view all your projects and manage them. To view project details, click into any of them.
You'll need these if you're using the API to connect to your database. Store them securely.
Keep your API Key a secret! Only use it on a secure server environment.
Make sure to replace `` and ``.
```js GET Project Details theme={null}
fetch("https://api.basic.tech/project/", {
headers: {
"Authorization": "Bearer ",
},
}).then(response => response.json())
.then(data => console.log(data));
```
# Create your schema
There are two ways to create your schema, either using the table UI, or by using the code editor. Let's explore the table UI first.
Give your table a unique name, and add fields to it. Fields need to have a unique name within each table, and you can use the dropdown to select the type of each field.
Fields can take on the following types: `string`, `number`, `boolean`, `json`.
You can also edit your schema with the code editor.
Once you've added your tables, you can publish your changes. Upon publishing, you'll notice your version number has been automatically incremented.
You may also discard changes if you'd like to revert to the existing version.
# Next steps
You can now start building your app!
If you are using a React app, we recommend you use the [React SDK](/basic-react/basic-react-sdk) to get started.
Otherwise, you can use the [API](/basic-restapi/basic-api) for any language.
# Filtering, ordering, pagination
Source: https://docs.basic.tech/basic-restapi/api-filtering
Learn how to filter results using the APIs
## Querying
Chain after `GET https://api.basic.tech/account/{project_id}/db/{table}` endpoint as query params.
### Filtering (`filter`)
```js theme={null}
const options = {method: 'GET'};
//example to filter where name equals John
fetch('https://api.basic.tech/account/{project_id}/db/{table}?name=eq.John', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
* **Method:** `filter(conditions)`
* **Supported Operators:** eq, neq, gt, gte, lt, lte, like, ilike, in, not (see [list of operators](#supported-operators))
### Ordering (`order`)
```js theme={null}
const options = {method: 'GET'};
//example to filter where created_at is descending, and name is ascending
fetch('https://api.basic.tech/account/{project_id}/db/{table}?order=created_at.desc,name.asc', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
* **Method:** `order(field, direction?)` (`direction` defaults to 'asc')
### Pagination (`limit`, `offset`)
```js theme={null}
const options = {method: 'GET'};
//example to filter where limit is 10, offset is 20
fetch('https://api.basic.tech/account/{project_id}/db/{table}?limit=10&offset=20', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
* **Methods:** `limit(count)`, `offset(count)`
## Supported operators
| Operator | Description | Example |
| -------- | -------------------------- | ------------------------ |
| `eq` | Equality | `status=eq.active` |
| `neq` | Not equal | `status=neq.deleted` |
| `gt` | Greater than | `priority=gt.3` |
| `gte` | Greater than or equal | `priority=gte.3` |
| `lt` | Less than | `priority=lt.3` |
| `lte` | Less than or equal | `priority=lte.3` |
| `like` | Pattern (case sensitive) | `title=like.*project*` |
| `ilike` | Pattern (case insensitive) | `title=ilike.*todo*` |
| `in` | Value in set | `tags=in.work,important` |
| `not` | Negation | `tags=not.eq.work` |
## Limitations
* Only one filterable condition per field per query (no compound filters across multiple fields)
* Range filters (e.g., `?field=gte.x<e.y`) not supported
* Method chaining order is flexible, but all must be chained after `GET https://api.basic.tech/account/{project_id}/db/{table}`
# Schema
Source: https://docs.basic.tech/basic-restapi/api-schema
Deeper dive into how schemas work in Basic
# Overview
In Basic, we declare schemas in a JSON format as part of the `basic.config.ts` or `basic.config.js` file that lives in your repository.
These `basic.config` files can be committed to git, and unlock "schema-as-code" capabilities for your project.
Basic schemas are very similar to SQL/Postgres tables with the following differences:
* no separate users table required (Basic's datastore per user model already ties data to each specific user)
* no need to declare RLS
* `id` and `created_at` fields are created automatically for you
Schemas would follow this general format:
The top level requires three parameters:
### project\_id
This is your project ID, which you can find in the Basic dashboard, or will be auto-generated for you if you had used the [CLI to create your project](/get-started/cli).
### version
This will be used for versioning your schema. It should be an integer that starts at 0 and increments by 1 for each new version.
# Tables
Let's double click into the tables to learn how they are declared in the schema.
Each table is declared as an object where the key is the table name and the value is an object with the following two parameters:
### type
This refers to the type of database table you are creating. At the moment, Basic only supports `collection`, which will operate similarly to MongoDB collections.
### fields
Fields are the columns of the table. They will be declared as an object of objects. Learn more about the fields below.
# Fields
Each field is declared as an object where the key is the field name and the value is an object with the following 2 parameters:
Default values for fields are not supported yet. Nesting fields is not supported; please use a flat structure for field declarations.
### type
This is the data type of the field. Basic supports the following field types: `string`, `number`, `boolean`, `json`.
### indexed
This lets you control if the field should be indexed in the user's client database for local-first benefits. For now, this should always be set to `true`.
# Example
When put all together, your schema could look like this:
```javascript basic.config.ts theme={null}
export const schema = {
project_id: '1234567890',
version: 0,
tables: {
comments: {
type: 'collection',
fields: {
title: {
type: 'string',
indexed: true
},
body: {
type: 'string',
indexed: true
},
rating: {
type: 'number',
indexed: true
}
}
},
items: {
type: 'collection',
fields: {
name: {
type: 'string',
indexed: true
},
quantity: {
type: 'number',
indexed: true
},
available: {
type: 'boolean',
indexed: true
}
}
}
}
}
```
# ⚡️ Quickstart
Source: https://docs.basic.tech/basic-restapi/basic-api
Integrate Basic into your project using REST APIs
First, check if there's an existing Basic SDK for your technology stack and use it instead for a better dev experience!
# Summary
There are two main steps to integrating Basic into your app: implement Basic Auth, then call the database APIs with the user's access token.
Implement signup / login flows for your app's users
Write and display data safely in your app
# Basic Auth APIs guide
Basic uses OAuth 2.0 for authentication. For more details, read about [Basic Auth](/readings/auth-basic#introduction-to-oauth-2-0).
In this guide, you will:
1. Use "redirect to sign in" endpoint to redirect users to account creation / login flow
2. Extract code from URL
3. Use "get auth token" endpoint with extracted code
4. Store auth token object for future API calls
Create a button that redirects the user to the Basic Auth page, with the following required parameters (more info on [required parameters](/readings/auth-basic#required-parameters)):
* `client_id='YOUR_CLIENT_ID'`
* `redirect_uri='YOUR_REDIRECT_URI'`
* `response_type=code`
* `scope=profile`
* `state='YOUR_STATE'`
Redirect URL template: `https://api.basic.tech/auth/authorize?response_type=code&redirect_uri=YOUR_REDIRECT_URI&scope=profile&state=YOUR_STATE&client_id=YOUR_CLIENT_ID`
Replace `YOUR_CLIENT_ID`, `YOUR_REDIRECT_URI`, and `YOUR_STATE` with your own values. `scope` must be a space-separated list (for example `profile` alone, or `profile` plus datastore scopes your app needs). The live contract matches the [OpenAPI spec](/api-reference/auth/redirect-to-sign-in) (`GET /auth/authorize`).
**PKCE (recommended):** You may include `code_challenge` and `code_challenge_method=S256` on the authorize request and the matching `code_verifier` when exchanging the code at `/auth/token`. See [Basic Auth](/readings/auth-basic) for the full OAuth flow.
```bash cURL theme={null}
curl --request GET \
--url 'https://api.basic.tech/auth/authorize?response_type=code&redirect_uri='YOUR_REDIRECT_URI'&scope=profile&state='YOUR_STATE'&client_id='YOUR_CLIENT_ID''
```
```python Python theme={null}
import requests
url = "https://api.basic.tech/auth/authorize"
querystring = {"response_type":"code","client_id":"YOUR_CLIENT_ID","redirect_uri":"YOUR_REDIRECT_URI","scope":"profile","state":"YOUR_STATE"}
response = requests.request("GET", url, params=querystring)
print(response.text)
```
```javascript JavaScript theme={null}
const options = {method: 'GET'};
fetch('https://api.basic.tech/auth/authorize?response_type=code&client_id='YOUR_CLIENT_ID'&redirect_uri='YOUR_REDIRECT_URI'&scope=profile&state='YOUR_STATE'', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```php PHP theme={null}
"https://api.basic.tech/auth/authorize?response_type=code&client_id='YOUR_CLIENT_ID'&redirect_uri='YOUR_REDIRECT_URI'&scope=profile&state='YOUR_STATE'",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.basic.tech/auth/authorize?response_type=code&client_id='YOUR_CLIENT_ID'&redirect_uri='YOUR_REDIRECT_URI'&scope=profile&state='YOUR_STATE'"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.get("https://api.basic.tech/auth/authorize?response_type=code&client_id='YOUR_CLIENT_ID'&redirect_uri='YOUR_REDIRECT_URI'&scope=profile&state='YOUR_STATE'")
.asString();
```
The authorization code will be in the URL as a `code` query parameter.
For example:
`https://your-app.com/callback?code=1234567890`.
You need to extract the code from the URL and use it in the next step.
```bash cURL theme={null}
# Assuming the URL is stored in a variable
URL="https://your-app.com/callback?code=1234567890"
CODE=$(echo $URL | grep -o 'code=[^&]*' | cut -d'=' -f2)
echo $CODE
```
```python Python theme={null}
from urllib.parse import urlparse, parse_qs
# Example URL
url = "https://your-app.com/callback?code=1234567890"
# Parse the URL and extract the code parameter
parsed_url = urlparse(url)
code = parse_qs(parsed_url.query)['code'][0]
print(code)
```
```javascript JavaScript theme={null}
// Example URL
const url = "https://your-app.com/callback?code=1234567890";
// Extract the code parameter
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
console.log(code);
```
```php PHP theme={null}
```
```go Go theme={null}
package main
import (
"fmt"
"net/url"
)
func main() {
// Example URL
rawURL := "https://your-app.com/callback?code=1234567890"
// Parse the URL and extract the code parameter
parsedURL, _ := url.Parse(rawURL)
code := parsedURL.Query().Get("code")
fmt.Println(code)
}
```
```java Java theme={null}
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// Example URL
String url = "https://your-app.com/callback?code=1234567890";
// Parse the URL and extract the code parameter
String query = URI.create(url).getQuery();
Map params = new HashMap<>();
for (String param : query.split("&")) {
String[] pair = param.split("=");
params.put(pair[0], URLDecoder.decode(pair[1], StandardCharsets.UTF_8));
}
String code = params.get("code");
System.out.println(code);
}
}
```
Make a post request to `/auth/token` with the authorization code in the body. The response will be a JSON object with the access token and a refresh code.
Token URL template:
`https://api.basic.tech/auth/token`
Make sure to replace `YOUR_CODE` with your own values
```bash cURL theme={null}
curl --request POST \
--url 'https://api.basic.tech/auth/token' \
--header 'Content-Type: application/json' \
--data '{"code":"YOUR_CODE"}'
```
```python Python theme={null}
import requests
url = "https://api.basic.tech/auth/token"
payload = {"code": "YOUR_CODE"}
headers = {"Content-Type": "application/json"}
response = requests.request("POST", url, json=payload, headers=headers)
print(response.text)
```
```javascript JavaScript theme={null}
const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code: 'YOUR_CODE'})
};
fetch('https://api.basic.tech/auth/token', options)
.then(response => response.json())
.then(response => console.log(response))
```
```php PHP theme={null}
"https://api.basic.tech/auth/token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "{\n \"code\": \"YOUR_CODE\"\n}",
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
```
```go Go theme={null}
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.basic.tech/auth/token"
payload := strings.NewReader("{\n \"code\": \"YOUR_CODE\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
```
```java Java theme={null}
HttpResponse response = Unirest.post("https://api.basic.tech/auth/token")
.header("Content-Type", "application/json")
.body("{\n \"code\": \"YOUR_CODE\"\n}")
.asString();
```
The response from the token endpoint will look like this:
```json theme={null}
{
"access_token": "",
"token_type": "",
"expires_in": 123,
"refresh_token": ""
}
```
# Basic database APIs guide
The [database APIs](/api-reference/project-users/create-item) are easy to use.
Make sure to check the validity of the `access_token`, and pass it in the Header of your API call.
If the `access_token` is expired, you'd repeat step 3 of the [Auth APIs guide](/basic-restapi/basic-api#basic-auth-apis-guide) but using the `refresh_token` instead of the extracted code. This will return you a fresh new `access_token`.
Here's how to validate and use the access token in different programming languages:
```bash cURL theme={null}
# Note: For bash, you'll need to use a JWT decoding tool like jq or jwt-cli
# Example using jwt-cli:
# if jwt decode YOUR_ACCESS_TOKEN | jq '.exp > now' | grep -q "true"; then
# # Token is valid, proceed with API calls
curl --request GET \
--url 'https://api.basic.tech/your-endpoint' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
# else
# # Token is expired, refresh it
curl --request POST \
--url 'https://api.basic.tech/auth/token' \
--header 'Content-Type: application/json' \
--data '{"refresh_token":"YOUR_REFRESH_TOKEN"}'
```
```python Python theme={null}
import jwt
import time
import requests
def refresh_token(refresh_token):
response = requests.post(
'https://api.basic.tech/auth/token',
json={'refresh_token': refresh_token},
headers={'Content-Type': 'application/json'}
)
return response.json()
def is_token_valid(access_token):
try:
# Decode the token without verification
decoded = jwt.decode(access_token, options={"verify_signature": False})
# Check if token is expired
return decoded['exp'] > time.time()
except:
return False
# Example usage
if is_token_valid(access_token):
# Token is valid, proceed with API calls
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get('https://api.basic.tech/your-endpoint', headers=headers)
else:
# Token is invalid, refresh it
new_tokens = refresh_token(refresh_token)
access_token = new_tokens['access_token']
refresh_token = new_tokens['refresh_token']
# Now use the new access token
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get('https://api.basic.tech/your-endpoint', headers=headers)
```
```javascript JavaScript theme={null}
import jwtDecode from 'jwt-decode';
async function refreshToken(refreshToken) {
const response = await fetch('https://api.basic.tech/auth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh_token: refreshToken })
});
return await response.json();
}
function isTokenValid(accessToken) {
try {
const decoded = jwtDecode(accessToken);
// Check if token is expired
return decoded.exp * 1000 > Date.now();
} catch (error) {
return false;
}
}
// Example usage
if (isTokenValid(accessToken)) {
// Token is valid, proceed with API calls
const response = await fetch('https://api.basic.tech/your-endpoint', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
} else {
// Token is invalid, refresh it
const newTokens = await refreshToken(refreshToken);
accessToken = newTokens.access_token;
refreshToken = newTokens.refresh_token;
// Now use the new access token
const response = await fetch('https://api.basic.tech/your-endpoint', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
}
```
```php PHP theme={null}
$refreshToken
]));
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
function isTokenValid($accessToken) {
try {
$decoded = JWT::decode($accessToken, null, false);
return $decoded->exp > time();
} catch (Exception $e) {
return false;
}
}
// Example usage
if (isTokenValid($accessToken)) {
// Token is valid, proceed with API calls
$ch = curl_init('https://api.basic.tech/your-endpoint');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken
]);
$response = curl_exec($ch);
curl_close($ch);
} else {
// Token is invalid, refresh it
$newTokens = refreshToken($refreshToken);
$accessToken = $newTokens['access_token'];
$refreshToken = $newTokens['refresh_token'];
// Now use the new access token
$ch = curl_init('https://api.basic.tech/your-endpoint');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken
]);
$response = curl_exec($ch);
curl_close($ch);
}
?>
```
```go Go theme={null}
package main
import (
"fmt"
"time"
"net/http"
"io/ioutil"
"encoding/json"
"github.com/golang-jwt/jwt/v5"
)
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func refreshToken(refreshToken string) (*TokenResponse, error) {
payload := map[string]string{
"refresh_token": refreshToken,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.basic.tech/auth/token", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var tokenResp TokenResponse
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
return &tokenResp, err
}
func isTokenValid(accessToken string) bool {
token, _, err := jwt.NewParser().ParseUnverified(accessToken, jwt.MapClaims{})
if err != nil {
return false
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
exp := claims["exp"].(float64)
return int64(exp) > time.Now().Unix()
}
return false
}
func main() {
if isTokenValid(accessToken) {
// Token is valid, proceed with API calls
req, _ := http.NewRequest("GET", "https://api.basic.tech/your-endpoint", nil)
req.Header.Add("Authorization", "Bearer "+accessToken)
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
} else {
// Token is invalid, refresh it
newTokens, err := refreshToken(refreshToken)
if err != nil {
fmt.Println("Error refreshing token:", err)
return
}
accessToken = newTokens.AccessToken
refreshToken = newTokens.RefreshToken
// Now use the new access token
req, _ := http.NewRequest("GET", "https://api.basic.tech/your-endpoint", nil)
req.Header.Add("Authorization", "Bearer "+accessToken)
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
}
```
```java Java theme={null}
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.util.Date;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import com.google.gson.Gson;
class TokenResponse {
String access_token;
String refresh_token;
}
public class TokenValidator {
private static final HttpClient client = HttpClient.newHttpClient();
private static final Gson gson = new Gson();
public static TokenResponse refreshToken(String refreshToken) throws Exception {
String json = String.format("{\"refresh_token\":\"%s\"}", refreshToken);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.basic.tech/auth/token"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(response.body(), TokenResponse.class);
}
public static boolean isTokenValid(String accessToken) {
try {
Claims claims = Jwts.parserBuilder()
.build()
.parseClaimsJwt(accessToken)
.getBody();
Date expiration = claims.getExpiration();
return expiration.after(new Date());
} catch (Exception e) {
return false;
}
}
public static void main(String[] args) {
if (isTokenValid(accessToken)) {
// Token is valid, proceed with API calls
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.basic.tech/your-endpoint"))
.header("Authorization", "Bearer " + accessToken)
.build();
try {
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
}
} else {
// Token is invalid, refresh it
try {
TokenResponse newTokens = refreshToken(refreshToken);
accessToken = newTokens.access_token;
refreshToken = newTokens.refresh_token;
// Now use the new access token
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.basic.tech/your-endpoint"))
.header("Authorization", "Bearer " + accessToken)
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
```
# Set up your Basic account
Source: https://docs.basic.tech/get-started/adminportal
Using the admin portal to create a new project.
# Create your project
Head to [https://app.basic.tech](https://app.basic.tech) to begin creating your project.
Login with your Basic account. If you don't have one yet, signup to Basic.
After creating your Basic account, head to the Dashboard and create a new project.
Use the form to create a project with a unique name and slug. Make sure not to leave the name or slug fields blank.
This page allows you to view all your projects and manage them. To view project details, click into any of them.
You'll need these if you're using the API to connect to your database. Store them securely.
Keep your API Key a secret! Only use it on a secure server environment.
Make sure to replace `` and ``.
```js GET Project Details theme={null}
fetch("https://api.basic.tech/project/", {
headers: {
"Authorization": "Bearer ",
},
}).then(response => response.json())
.then(data => console.log(data));
```
# Create your schema
There are two ways to create your schema, either using the table UI, or by using the code editor. Let's explore the table UI first.
Give your table a unique name, and add fields to it. Fields need to have a unique name within each table, and you can use the dropdown to select the type of each field.
Fields can take on the following types: `string`, `number`, `boolean`, `json`.
You can also edit your schema with the code editor.
Once you've added your tables, you can publish your changes. Upon publishing, you'll notice your version number has been automatically incremented.
You may also discard changes if you'd like to revert to the existing version.
# Next steps
You can now start building your app!
If you are using a React app, we recommend you use the [React SDK](/basic-react/basic-react-sdk) to get started.
Otherwise, you can use the [API](/basic-restapi/basic-api) for any language.
# Set up the Basic CLI (optional)
Source: https://docs.basic.tech/get-started/cli
Using the CLI or code editor LLMs to manage projects.
## Install the CLI
First, install the CLI using your terminal.
```bash theme={null}
npm i -g @basictech/cli
```
Run `basic login` to create your basic admin account. You will be redirected to a web page to login.
```bash theme={null}
basic login
```
# Create / integrate a project
Navigate to your project directory and run `basic init` to create your project.
```bash theme={null}
cd project-name
```
```bash theme={null}
basic init
```
You will be prompted to choose between creating a new project or integrating an existing project.
Choose your tech stack and project name.
That's it! You now have a new project created. You'll even notice a new `basic.config.ts` / `basic.config.js` file in your project directory with your project ID included!
If you run `basic init` in a codebase with an existing `basic.config.ts` file (e.g., created using via [create-lofi-app](/basic-react/create-lofi-app)), you'll notice that the project\_id in your `basic.config.ts` file gets automatically updated.
# Other CLI commands
**View all commands**
```bash theme={null}
basic help
```
Other commands include:
* `basic account` - Show account information
* `basic logout` - logout from your basic account
* `basic status` - Show login status
* `basic projects` - list your projects
* `basic version` - Show CLI version
* `basic update` - Update CLI to the latest version
* `basic debug` - Show Basic config directory location
# What is Basic?
Source: https://docs.basic.tech/get-started/welcome
Build personalized apps and agents with Basic.
the web in your hands
## Features and benefits
Basic is a purpose-built backend for personalization of apps and agents. Instead of one shared database for all users, Basic uses a **federated model**: a dedicated datastore per user in the cloud. You read and write through SDKs or APIs with built-in auth and permissioning.
* **Zero infrastructure**: No database to provision for that layer—CRUD via SDKs and HTTP.
* **Shared user context across apps**: When users connect multiple apps to Basic, you can work with the same permissioned datastore model (see [Why Basic?](/readings/why-basic)).
* **Built-in auth and permissioning**: OAuth flows and scoped tokens, with row-level expectations documented in [Managing permissions](/readings/permissioning).
* **Federated, per-user datastores**: Portable, user-centric data for deep personalization.
* **Responsive clients**: Local-first sync where you use the web/mobile SDKs; near-instant UI with offline-friendly patterns.
*On the roadmap: broader real-time collaboration patterns, federated learning, and more.*
## Use cases
* **AI apps and agents**: Store and retrieve user context across sessions safely.
* **AI app builders**: Hand thousands of end-user apps a consistent auth + data layer.
* **AI wearables and sensitive UX**: Keep user data scoped to their store without leaking it across tenants.
* **Embedded AI**: Personalize models using permissioned user context, not only generic defaults.
* **Personal and productivity software**: Sync and offline-friendly UX for competitive polish.
## Getting started
First, [set up your Basic account and project](/get-started/adminportal). Optionally [install the Basic CLI](/get-started/cli).
Then pick docs for your stack from the **Documentation** dropdown:
| Framework / API | Support |
| ------------------- | :-----: |
| React | Yes |
| Next.js | Yes |
| Expo / React Native | Yes |
| REST | Yes |
| Management APIs | Yes |
*More client runtimes are on the [roadmap](/readings/others/roadmap).*
**Implementation guides:**
Integrate Basic into your React project
Integrate Basic into your Next.js project
Integrate Basic into your Expo / React Native project
Integrate Basic over HTTP
## Side quests
Core concepts behind user-owned data
Feedback or help getting started
Product updates
# Basic Auth
Source: https://docs.basic.tech/readings/auth-basic
Overview of the Basic Auth
This is primarily for developers using the Basic API. The client SDKs handle all these aspects of OAuth 2.0 for you.
# Introduction to OAuth 2.0
Basic uses OAuth 2.0 for authentication. This widely adopted framework allows users to securely grant permissions to multiple applications using a single account, similar to "Login with Google". It ensures user data security while enabling controlled access for third-party apps, without directly handling sensitive login credentials.
### Authorization
First, an application redirects the user to the Authorization Server. Here, the user enters their login credentials of their existing account (e.g., Google, Apple, BasicID, etc.), and explicitly grants permissions to your app.
The benefit of using a service for Authorization is that they handle storing user credentials securely, validating passwords, account recovery, etc.
The Authorization Server then issues an authorization code to the application, which has a one-time use and is exchanged for an access token (also known as JWTs) and a refresh code.
These tokens and codes should be extracted from the callback URL and are generally stored in your user's cache or cookies.
### Access tokens
Access tokens are a short-lived JWT (with Basic, this is 1 hour) that can be used to make secure and authenticated requests to protected information. They are used to make requests to the API via the `Authorization` header (e.g., `Authorization: Bearer 'YOUR_ACCESS_TOKEN'`).
Before making a request, you should validate if the access token is still valid - if it has expired, you would use the refresh token to get a new access token without requiring the user to re-authenticate (i.e., without re-entering their login credentials).
### Refresh code
Refresh codes can be used to obtain a new access token via the `/token` endpoint when the current access token expires. This regenerates a new access token and refresh code.
***
# Required parameters
Here are the required parameters you need to set on the "Sign in with Basic" button:
* `client_id`
* `redirect_uri`
* `response_type`
* `scope`
* `state`
### client\_id
Your application's client ID from [admin.basic.tech](https://admin.basic.tech) (project settings).
### redirect\_uri
This is the URL that the user will be redirected to after they sign up or sign in. This would generally be a URL on your app.
### response\_type
This must always be set to `code`, so that you can receive and extract an authorization code in the callback URL.
### scope
Scopes are used to specify the information that your application requires. For Basic, you can specify `profile` to request the user's email address and other information.
### state
The state parameter is used to prevent CSRF attacks. It is a random string that is used to verify that the callback request is legitimate. It is recommended that you use a secure random string generator to generate a state parameter, and that you verify that the state parameter that you receive in the callback URL is the same as the one you sent.
### PKCE (optional, recommended)
SHA-256 hash of your `code_verifier`, base64url-encoded. Send together with `code_challenge_method=S256` on `/auth/authorize`, then send the original `code_verifier` in the body of `POST /auth/token` when exchanging the code.
The [REST quickstart](/basic-restapi/basic-api) and the [redirect-to-sign-in API reference](/api-reference/auth/redirect-to-sign-in) describe the full parameter set.
# Local-first sync
Source: https://docs.basic.tech/readings/local-first-sync
How Basic's clients keep data fast, offline-friendly, and in sync.
# Summary
Local-first architecture refers to a modern development pattern where primary read and write operations occur to a local database on clients' devices while data is synced across devices and users automatically in the background.
This provides benefits like instant UI, offline support, and real-time capabilities while still syncing to the cloud automatically.
With Basic, you get these behaviors from the **sync** mode of our client SDKs (for example [React](/basic-react/basic-react-sdk) or [Next.js](/basic-nextjs/basic-nextjs-sdk)). **Remote** mode talks to the server directly without a local replica—see [Database modes](/basic-nextjs/nextjs-modes).
# What is local-first sync?
Local-first architecture is a hybrid approach that believes in leveraging the capabilities of client devices to enhance the cloud experience, rather than solely relying on the cloud. This is in direct juxtaposition to local-only or cloud-only architectures.
| Native property | Local-only | Local-first | Cloud |
| ------------------------ | :--------: | :---------: | :---: |
| Low-latency / Instant UI | ✓ | **✓** | |
| Offline support | ✓ | **✓** | |
| Real-time sync | | **✓** | ✓ |
| Multi-device support | | **✓** | ✓ |
| Backup to cloud | | **✓** | ✓ |
With the rise of really powerful web apps that demand more responsive UIs while needing to be highly collaborative (such as Figma, Linear, Notion, etc.), local-first sync has become a popular approach for both adopters and tool makers.
# Benefits of local-first sync
### Instant and snappy UI
One of the immediately noticeable benefits of local-first sync is how snappily the UI responds to user actions.
For example, in the Regular UI, there's a small delay between when a user clicks the "Add" button and when the UI updates to reflect the new item. This is because the action requires a network request to the server to update the database, for the client to receive the new state, and the UI to re-render.
In the Instant UI example, the UI updates immediately after the user clicks the "Add" button because the action only requires updating the local database. Changes are synced to the cloud in the background without any visible latency to the user.
### Fewer network calls
In a local-first sync environment, every primary read/write operation occurs directly to the local database, drastically reducing the number of network calls to the server.
This change in data operations has two primary benefits:
Fewer network calls across all your users mean a lower load on the server, therefore lowering server costs.
Having to rely on the network for primary read/write operations introduces many points of failure. Local-first sync overcomes issues like lost connectivity, unreachable servers, and slow networks that often make apps unusable.
Instead, local-first architectures generally leverage a continuous connection (e.g., Basic uses WebSocket) to the server to sync data.
### Simpler architecture
Adopting the Basic local-first sync engine abstracts the need for a bloated backend.
Traditional stacks require spinning up and maintaining a backend to make API calls to the database. In contrast, you can use the Basic local-first sync architecture to have your app's frontend interact directly with your client's local database while the Basic sync engine manages cloud database communication.
The simpler architecture translates to faster development cycles for your developers with less backend code to maintain.
### Automatic state management
Traditionally, developers need to update state with the data fetched from their API requests.
Basic's client SDKs contain hooks that listen for changes and automatically update state on the frontend. This translates to significantly less code for developers to write, test, and maintain as is evident from the example above.
Now, you can access data as simply as `db.collection('tablename').get()` without worrying about updating state.
### Offline support
Connection gets spotty for users in various situations - if they're going through a tunnel, hiking through a less populated park, or even just in a crowded coffee shop.
Rather than eliminating your users' ability to use your app in these situations, local-first sync ensures they can continue working with your app.
This can reduce their frustration and save your developers the time of implementing loading states for every UI element they build.
### Real-time capabilities
The future of the internet has proven itself to be collaborative.
But building real-time capabilities is very hard and takes away your developers' time from building other features.
Basic's local-first sync engine comes with built-in real-time capabilities so your users can use your app with their friends and coworkers with no additional engineering on your part.
# Trade-offs with local-first sync
It would be insincere to discuss a new technology without mentioning its trade-offs. Here are a few we've identified:
Local-first apps require more storage on the client device as they store more data locally.
As more of the internet becomes local-first, more storage will be required to support this.
However, storage has gotten really cheap, with phones and laptops coming with higher storage capacities by default.
Additionally, good partial-sync algorithms (as those provided by Basic) manage this automatically on behalf of developers and users.
Local-first apps offload a lot of primary read/write operations to the client device.
While most modern devices are more than capable of handling this, some older devices or devices with limited processing power may face faster battery drain.
Basic's sync engine is designed to be efficient in terms of processing power, and we regularly make optimizations to ensure that your app remains fast and responsive even as its data grows.
# How to use local-first with Basic
All of Basic's local-first sync capabilities come built-in with our client SDKs.
Start with the [React](/basic-react/basic-react-sdk) or [Next.js](/basic-nextjs/basic-nextjs-sdk) quickstart (sync mode), or use the [REST API](/basic-restapi/basic-api) when you are not using a local-first client.
# External resources
Source: https://docs.basic.tech/readings/others/reading
Background reading on local-first and user-owned data
Curated links for context on **local-first software**, **user-owned data**, and adjacent ecosystems. Basic does not endorse every project below; they are useful for comparison and learning.
## Local-first and sync
* [Ink & Switch — local-first](https://www.inkandswitch.com/local-first/) — foundational essay on the pattern.
* [localfirstweb.dev](https://localfirstweb.dev/) — community and resources.
* [Cambria (Ink & Switch)](https://www.inkandswitch.com/cambria) — schema evolution and JSON interoperability.
## User-owned and decentralized identity
* [Solid](https://solidproject.org/) — pods and linked data.
* [Autonomous data](https://autonomous-data.noeldemartin.com/introduction.html) — framing for user-held data.
* [unhosted.org](https://unhosted.org/) — “no backend” / client-side apps with remote storage.
* [0data.app](https://0data.app) — catalog of zero-knowledge / user-owned patterns.
* [Fission](http://fission.codes), [remoteStorage](http://remotestorage.io), [Matrix](http://matrix.org) — related stacks and protocols.
## Tools in the local-first space
* [Instant](https://www.instantdb.com/), [Electric SQL](https://electric-sql.com/), [PowerSync](https://www.powersync.com/) — sync and realtime databases.
* [Obsidian](https://obsidian.md) — local-first notes (product example).
# Roadmap
Source: https://docs.basic.tech/readings/others/roadmap
High-level product direction for Basic.
Public direction (not commitments or ETAs):
* **SDKs and clients**: Broader first-party support (for example Python, native iOS/Android) beyond React, Next.js, Expo, and REST.
* **Collaboration**: Richer real-time and multiplayer-style patterns on top of sync.
* **Schema and types**: Stronger TypeScript and codegen from schema definitions.
* **History and undo**: Versioning primitives for user-visible undo/redo style flows.
* **Sync**: Smarter partial sync so large accounts do not need the full corpus on every device.
* **Deployment**: Self-hosted and bring-your-own-infrastructure options for datastores.
For what ships when, follow [Twitter / X](https://twitter.com/basic_db) and [Discord](https://discord.gg/PuWD3zhsCk)—this list is intentionally high level.
# Managing permissions
Source: https://docs.basic.tech/readings/permissioning
How users will be able to manage permissions to their data
Permission UX and fine-grained scope strings continue to evolve. Treat this page as the conceptual model; exact scope names and dashboard controls may change—verify against your project settings and the live OAuth flow.
# How it works
Users sign in and **grant scopes** to your app. Access tokens encode what the app may do. Users can **revoke** access from their account when they no longer trust an app.
### Flow
1. Application initiates login / auth flow, which redirects users to a Basic SSO page
2. User can select how much access they want to give
3. The scope is defined into a self-verifiable token (JWT) that is sent to the application
The token is cryptographically signed and is verifiable (i.e., it cannot be forged, and developers can independently verify that it came from the respective users)
4. The application then uses this token to make requests to the user's datastore.
The application simply includes the token as an auth header in its API requests
### Scope
Usually, an application will specify what it wants to request. At minimum, this is set to `profile` to return general profile info.
Scope can be extended to include access to the database, which is organized in the following hierarchy: `database > table > row > field`.
For example, an application might request the following scope:
* `profile`
* `database_id.table_id` = read
* `database_id.table_id.row_id` = read, write
In this example, the application can read anything under a specified table in the database, but only has write access to one row/object.
This process is modeled after [UCANs](http://ucan.xyz/)
# Personal Data Stores (PDS)
Source: https://docs.basic.tech/readings/personal-data-stores
What is a PDS in Basic
# Summary
PDS' are independent cloud data stores per user.
An assigned primary user is the ultimate owner of the data within that data store.
Data may be shared with apps, devices, and other users, and the decision to do so is always made by (or reverted by) the primary user.
# What is a Basic Data Store?
We designed Basic Data Stores so that every user is the proprietor of their own data.
The data store above is depicted for an individual. For businesses, the data store would be owned by primary admins and the same principles apply.
Fundamentally, a PDS is a data store that’s dedicated to a user.
This database is independent of any application that it is connected to (in that it doesn’t live on the servers or be owned by applications in the way a traditional database like Firebase, MongoDB, etc. may be).
Instead, it’s ownership belongs to the user that’s assigned to it as a primary user.
This contrasts with the traditional centralized ownership model of data as shown below:
It’s best to think of a database as a bucket of data.
In a centralized model, whoever is building an app (they may use databases like Firebase, MongoDB, etc.) holds onto that bucket of data, putting in all the information about their users, and the data generated by their users.
The developers of that app can do with that bucket as they see fit, and at the end of the day, they own that bucket and its contents.
For example, if your user is typing up a document, they are not only using your interface (frontend) to type and format the document, but also using your backend and data layers to store the data.
Data flows between the frontend, backend, and data layers as shown in App A.
The apps and websites they're using act as an intermediary that they have to interact with in order to retrieve or change their own data.
For example, if App B or C want to access the words they had typed up in Google Docs (App A), they would likely have to go through the Google Docs backend.
PDS' flips this paradigm and give every user their own bucket, where information about them and the data they generate is put into their own independent buckets.
Applications will request permission to read / write data to those buckets, but at any point users can sever those connections to any of their apps rather than relying on intermediary services for accessing their data.
# Benefits of PDS'
### Consolidation and a single user datastore
In traditional centralized data ownership models, user data is spread across multiple databases owned by different apps and websites (notice how User A's data is spread across Apps A, B, and C).
The data is likely to be in different formats, limiting its utility and increasing the complexity of integration.
With PDS', user data remains in a single space that can be queried (all the data from Apps A, B, and C that pertains to User A are stored in User A's data store).
If you were to build a word processor tool A (similar to Google Docs) using Basic, all the words that your users type up on the document are stored in their data store.
Any words they may type up in a different word processor tool B that also uses Basic for data would also be stored in the same data store.
Apps read and write only the collections and fields users authorize. Multiple apps can connect to the same user’s store when the user allows it, which helps **reuse context** (for example profile or notes) without each app holding a separate siloed copy—exact capabilities depend on schema and permissions.
### Control, ownership, and permanence
Traditionally, user data is generally only interactable through APIs of the respective services.
This approach relies on individual services to implement APIs, and to decide what data to share or not.
A mismatch of priorities between the type of data a service wants to make available through their APIs and the data a user may want to use would cause users to feel like their data is "walled off".
With PDS', a user's data remains with them regardless of the availability of APIs or the shuttering of services (in the diagram above, even if any of the connections to apps, devices, or users disappear, the data will still be available in User A's data store).
It will remain available for use with any future services - a deep dive into how Basic enables data permanence is coming soon.
### Simpler applications
Traditional apps and websites have larger and more complex backend and data layers because of having to manage data, auth, and other complex data operations for all their users.
PDS' allow developers to build apps and websites with much simpler architectures since they no longer need to own or manage user data.
With Basic, developers can focus on building better user experiences.
# FAQs
No. Human errors and social engineering compose of the vast majority of security breaches.
In traditional databases, the consequences of these sort of human-oriented attacks are much more dire since a single database tends to contain information about hundreds of thousands or millions of users.
In PDS, data is limited to only that of a single user.
For a hacker to obtain the equivalent scale of data, they’d have to conduct an attack on millions of users instead of relying on a single “weak point”, which is significantly more difficult and less rewarding for them.
On top of that, Basic’s protocol is designed to be safe and our user’s information is of our utmost priority and will continue to invest in ensuring they don’t fall privy to phishing or data breach attempts.
No.
Basic automatically syncs data across devices and the cloud when internet is available to ensure that data is always backed up.
Users don’t have to manually perform any backups of their data.
No, Basic is simple to use.
Users can manage their apps and permissions on Basic.id (feel free to check out what the experience would look like for them at [Basic.id](https://basic.id)!).
The interface to do so is simple and straightforward, and we’ll continue to invest in making this experience smoother for users.
In fact, if they never intend on changing any of their permissions, other than the OAuth flow to sign up to your app they never have to interact with Basic directly since it’s entirely optional.
And they can always in the future choose to access their data since it’s always in their control from the start even if they don’t take any explicit actions towards it - Basic is designed to retain data ownership for its users.
***
Basic provides open-source-oriented tooling (auth, datastore APIs, sync clients) so you can build applications where **user data stays user-centric** and permissioned, without standing up all of that infrastructure yourself.
# Schema management
Source: https://docs.basic.tech/readings/schema
Deeper dive into how schemas work in Basic
# Overview
In Basic, we declare schemas in a JSON format as part of the `basic.config.ts` or `basic.config.js` file that lives in your repository.
These `basic.config` files can be committed to git, and unlock "schema-as-code" capabilities for your project.
Schemas follow this general format:
The top level requires three parameters:
### project\_id
This is your project ID, which you can find in the Basic dashboard, or will be auto-generated for you if you had used the [CLI to create your project](/get-started/cli).
### version
This will be used for versioning your schema. It should be an integer that starts at 0 and increments by 1 for each new version.
# Tables
Let's double click into the tables to learn how they are declared in the schema.
Each table is declared as an object where the key is the table name and the value is an object with the following two parameters:
### type
This refers to the type of database table you are creating. At the moment, Basic only supports `collection`, which will operate similarly to MongoDB collections.
### fields
Fields are the columns of the table. They will be declared as an object of objects. Learn more about the fields below.
# Fields
Each field is declared as an object where the key is the field name and the value is an object with the following 2 parameters:
Default values for fields are not supported yet. Nesting fields is not supported; please use a flat structure for field declarations.
### type
This is the data type of the field. Basic supports the following field types: `string`, `number`, `boolean`, `json`.
### indexed
This lets you control if the field should be indexed in the user's client database for local-first benefits. For now, this should always be set to `true`.
# Example
When put all together, your schema could look like this:
```javascript basic.config.ts theme={null}
export const schema = {
project_id: '1234567890',
version: 0,
tables: {
comments: {
type: 'collection',
fields: {
title: {
type: 'string',
indexed: true
},
body: {
type: 'string',
indexed: true
},
rating: {
type: 'number',
indexed: true
}
}
},
items: {
type: 'collection',
fields: {
name: {
type: 'string',
indexed: true
},
quantity: {
type: 'number',
indexed: true
},
available: {
type: 'boolean',
indexed: true
}
}
}
}
}
```
# Why Basic?
Source: https://docs.basic.tech/readings/why-basic
Basic is a backend for **user-owned data**: each end user gets a dedicated datastore in the cloud, with OAuth sign-in, sync, and clear permission boundaries. Apps read and write that data through SDKs or HTTP APIs instead of running a separate database for every customer.
It fits products that need **personalization, cross-app context, or agents**—anywhere user-specific state should stay portable and under the user’s control.
## Problem
Typical app backends centralize user data with the vendor:
* **Fragmentation**: The same person’s data is copied and isolated per app.
* **Lock-in**: Hard to move data or reuse it across tools the user already uses.
* **Privacy and risk**: One breach or policy change affects many users at once.
* **Operational load**: You scale storage, backups, and compliance for all users’ rows in one place.
## What we optimize for
1. **User-owned stores**: Data is tied to the user; apps request access instead of owning the canonical copy.
2. **Portability**: Users can bring the same datastore to multiple apps that integrate Basic, within the permissions they grant.
3. **Built-in auth and scopes**: Sign-in and tokens reflect what each app is allowed to do.
4. **Developer speed**: Schema-as-code, client SDKs with local-first sync where supported, and REST for any stack.
We also care that the platform is **easy to adopt** (quick project setup, familiar document-style collections), **typed at the schema layer**, and **elastic** as your user base grows.
***
## How Basic differs
Traditionally, your app hosts one database and all user rows live there:
With Basic, each user has their own datastore; your app uses APIs and SDKs against that federated model:
### For users
1. **Control**: Revoke or narrow app access when they want.
2. **One place for their data**: Fewer duplicate profiles and silos.
3. **Sync and offline** (with client SDKs): Work locally; sync when online.
4. **Less vendor dependency**: Their data isn’t only inside one app’s servers.
### For developers
1. **Less central DB to scale**: Heavy reads/writes move to a model aligned with per-user stores.
2. **Security posture**: Sensitive payloads can stay scoped to user stores and tokens.
3. **Faster integration**: Auth, schema, and CRUD patterns are consistent across React, Next.js, Expo, and REST.
## How it works
### Auth
OAuth 2.0 (with PKCE where applicable). Users sign in through Basic; your app receives tokens with agreed scopes. Client SDKs wrap the flow; for custom stacks, see [Basic Auth](/readings/auth-basic).
### Data model
Today, projects define **collections** of JSON-shaped documents in `basic.config` (similar in spirit to Firebase or MongoDB collections), with indexed fields for sync and queries. The important difference is **per-user isolation** and **local-first clients** where you use the sync SDK.
***
For a product-focused overview, start with [What is Basic?](/get-started/welcome). For architecture detail, see [Personal data stores](/readings/personal-data-stores) and [Local-first sync](/readings/local-first-sync).
# Database (get, add, delete, etc.)
Source: https://docs.basic.tech/sdk-reference/expo-rn
Learn how to access data using the Expo / RN SDK
Use these methods on any table. You'd select a table using `db.from(*insert table name*)`, and chain it with any of the following methods:
* `getAll()`
* `get(id)`
* `add(value)`
* `update(id, value)`
* `replace(id, value)`
* `delete(id)`
Make sure not to create any `id` or `created_at` field since these are reserved and exist with every row. You can access them with the `get` operators
### Get all records
```js theme={null}
const allNotes = await db.from('notes').getAll();
```
* **Method:** `getAll()`
* **Returns:** Query builder for chaining (see [filtering and ordering](/basic-expo-rn/rn-filtering) for instructions)
### Get by ID
```js theme={null}
const note = await db.from('notes').get('note-id-here');
```
* **Method:** `get(id)`
* **Returns:** Record object or throws if not found
### Add a record
```js theme={null}
const newNote = await db.from('notes').add({...});
```
* **Method:** `add(value)`
* **Returns:** The newly added record
### Update a record
```js theme={null}
const updated = await db.from('notes').update('noteId', { completed: true })
```
* **Method:** `update(id, value)`
### Replace a record
```js theme={null}
const replaced = await db.from('notes').replace('noteId', {...})
```
* **Method:** `replace(id, value)`
### Delete a record
```js theme={null}
const deleted = await db.from('notes').delete('noteId')
```
* **Method:** `delete(id)`
# Components (BasicProvider)
Source: https://docs.basic.tech/sdk-reference/react-components
# BasicProvider
To enable the features of the Basic SDK, you need to wrap your app in a `` component.
```tsx main.tsx theme={null}
import { BasicProvider } from '@basictech/react'
import schema from './basic.config'
function App() {
return (
)
}
```
## Props
### schema
The Basic schema object containing your project configuration and table definitions. This should be imported from your `basic.config.ts` file.
The `project_id` is extracted from `schema.project_id`.
### project\_id
The project ID of your Basic project.
**Deprecated**: Project ID is now extracted from `schema.project_id`. This prop is kept for backward compatibility but can be omitted if your schema includes the project\_id.
### debug
Enable debug mode for additional logging to the console. Useful during development.
```tsx theme={null}
```
### dbMode
Determines which database implementation is used:
* **`'sync'`** (default): Uses IndexedDB + WebSocket for local-first sync. Works offline, provides real-time updates, and `useQuery` auto-refreshes.
* **`'remote'`**: Uses REST API calls directly to the server. No local storage, requires authentication for all operations, but has no IndexedDB dependencies.
```tsx theme={null}
// Use remote mode for SSR-heavy apps or when you don't need offline support
```
See [Database Modes](/basic-nextjs/nextjs-modes) for detailed comparison.
### storage
Custom storage adapter for persisting auth tokens. By default, uses `LocalStorageAdapter` which stores data in `localStorage`.
Implement the `BasicStorage` interface to use a custom storage solution (e.g., for React Native with AsyncStorage):
```typescript theme={null}
interface BasicStorage {
get(key: string): Promise
set(key: string, value: string): Promise
remove(key: string): Promise
}
```
```tsx theme={null}
import { BasicProvider, BasicStorage } from '@basictech/react'
import AsyncStorage from '@react-native-async-storage/async-storage'
const asyncStorageAdapter: BasicStorage = {
get: (key) => AsyncStorage.getItem(key),
set: (key, value) => AsyncStorage.setItem(key, value),
remove: (key) => AsyncStorage.removeItem(key)
}
```
### auth
Custom authentication configuration. Most apps omit this and use defaults.
```typescript theme={null}
type AuthConfig = {
scopes?: string | string[] // default: 'profile,email,app:admin'
pds_url?: string // PDS / auth host (default: 'https://pds.basic.id')
admin_url?: string // Admin API for telemetry (default: 'https://api.basic.tech')
ws_url?: string // Sync WebSocket (default: 'wss://pds.basic.id/ws')
/** @deprecated */ server_url?: string // use pds_url instead
}
```
```tsx theme={null}
```
### devToolbar
When `true`, shows the lazy-loaded **Basic dev toolbar** (schema status, useful shortcuts) in environments where it is allowed (for example localhost, `NODE_ENV=development`, or when `debug` is enabled on the provider). Export `BasicDevToolbar` from `@basictech/react` if you want to place it yourself.
***
## Full Example
```tsx main.tsx theme={null}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BasicProvider } from '@basictech/react'
import { schema } from './basic.config'
import App from './App'
createRoot(document.getElementById('root')!).render(
)
```
# Database (get, add, update, delete, etc.)
Source: https://docs.basic.tech/sdk-reference/react-db
# Overview
There are 7 methods you can use to interact with the database.
They are used in conjunction with the `db` property from the `useBasic()` hook, and are appended in the format of `db.collection('tablename').METHOD()`.
## Methods
### .get()
Fetch a single item from the table by its ID. Returns `null` if not found.
### .getAll()
Fetch all items from the table. Returns an empty array if no items exist.
In the **React / Next.js client SDK** (sync or remote mode), `.getAll()` loads items for that collection and `.filter()` applies your predicate **on the client** after fetch. For **server-side query parameters** on HTTP, use the [REST API filtering guide](/basic-restapi/api-filtering) against the PDS endpoints instead.
### .add()
Adds a new item to the table. The `id` is automatically generated by the server. Returns the created object with its new ID.
### .put()
Upsert (insert or replace) an item. The `data` object **must include an `id` field**. If an item with that ID exists, it will be replaced entirely. If not, a new item will be created. Returns the upserted object.
### .update()
Partially update an existing item by ID. Only the fields you provide will be updated—other fields remain unchanged. Returns the updated object, or `null` if the item was not found.
### .delete()
Deletes an item from the table by ID. Returns `true` if deleted, `false` if the item was not found.
### .filter()
Filter records using a predicate function. This fetches all records and filters client-side. Returns an array of matching objects.
***
# Examples
### Read items
There are 2 ways to read items from the database:
* `.get('ID_OF_ITEM')`: Fetches a single item by ID
* `.getAll()`: Fetches all items from the table
```tsx App.tsx theme={null}
import { useBasic, useQuery } from '@basictech/react'
import { useState } from 'react'
function App() {
// Import db from useBasic() inside your React component
const { db } = useBasic()
// Example 1: Use useQuery to "subscribe" to data from the database
// This will automatically re-render when data changes
const items = useQuery(() => db.collection('tablename').getAll())
// Example 2: Or get a single item by ID using async/await
const [singleItem, setSingleItem] = useState(null)
const getItem = async () => {
const item = await db.collection('tablename').get('ID_OF_ITEM')
if (item) {
setSingleItem(item)
console.log("Successfully fetched item: ", item)
} else {
console.log("Item not found")
}
}
return (
<>
{/* render items */}
>
)
}
```
### Add new item
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { db } = useBasic()
// Use .add() to add items - id is auto-generated
const addItem = async () => {
const newItem = await db.collection('tablename').add({ name: 'cutie' })
console.log("Created item with ID: ", newItem.id)
}
return (
<>
>
)
}
```
### Put (upsert) item
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { db } = useBasic()
// Use .put() to upsert items - requires id in data
const upsertItem = async () => {
const item = await db.collection('tablename').put({
id: 'specific-id-123',
name: 'updated name',
completed: true
})
console.log("Upserted item: ", item)
}
return (
<>
>
)
}
```
### Update item
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { db } = useBasic()
// Use .update() to partially update items
// Only the fields you provide will be changed
const updateItem = async () => {
const updated = await db.collection('tablename').update('ID_OF_ITEM', {
name: 'super cute'
})
if (updated) {
console.log("Successfully updated item: ", updated)
} else {
console.log("Item not found")
}
}
return (
<>
>
)
}
```
### Delete item
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { db } = useBasic()
// Use .delete() to delete items by ID
const deleteItem = async () => {
const deleted = await db.collection('tablename').delete('ID_OF_ITEM')
if (deleted) {
console.log("Successfully deleted item")
} else {
console.log("Item not found")
}
}
return (
<>
>
)
}
```
### Filter items
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
import { useState } from 'react'
function App() {
const { db } = useBasic()
const [completedItems, setCompletedItems] = useState([])
// Use .filter() to filter items client-side
const getCompletedItems = async () => {
const items = await db.collection('todos').filter(
(item) => item.completed === true
)
setCompletedItems(items)
console.log("Completed items: ", items)
}
return (
<>
>
)
}
```
***
# TypeScript Support
The `collection` method supports generics for type-safe database operations:
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
// Define your item type
type Todo = {
id: string
title: string
completed: boolean
createdAt: string
}
function App() {
const { db } = useBasic()
// Type-safe collection access
const todos = db.collection('todos')
const addTodo = async () => {
// TypeScript knows the shape of your data
const newTodo = await todos.add({
title: 'Learn Basic',
completed: false,
createdAt: new Date().toISOString()
})
console.log(newTodo.id) // TypeScript knows this exists
}
return
}
```
# Hooks (useBasic, useQuery)
Source: https://docs.basic.tech/sdk-reference/react-hooks
# useBasic
The `useBasic` hook is used to access authentication, database, and other Basic features.
Any of these properties can be accessed by destructuring the `useBasic` hook.
## Auth State
### isReady
Boolean that indicates when auth state has been determined. Use this as a loading state for your app while auth is being verified.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { isReady, isSignedIn } = useBasic()
if (!isReady) return
Loading...
return (
{isSignedIn ? "User is signed in" : "User is signed out"}
)
}
```
`isAuthReady` is a deprecated alias for `isReady` and will be removed in a future version.
### isSignedIn
Boolean that checks if a user is signed in. Only use after `isReady` is true.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { isReady, isSignedIn } = useBasic()
return (
<>
{isReady ?
{isSignedIn ? "User is signed in" : "User is signed out"}
:
Loading...
}
>
)
}
```
### user
The current user object containing the user's ID, email, and name. Returns `null` when not signed in.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { user } = useBasic()
console.log("Fetch user ID: ", user?.id)
console.log("Fetch user email: ", user?.email)
console.log("Fetch user name: ", user?.name)
return (
<>
{/* render items */}
>
)
}
```
**User type** (fields present depend on IdP / userinfo):
```typescript theme={null}
type User = {
sub?: string
name?: string
email?: string
picture?: string
}
```
### did
The authenticated user's Decentralized Identifier when available; `null` when not signed in or not provided by the identity layer.
### scope
Space-separated OAuth scopes granted on the current access token (for example `profile` plus datastore scopes). Use `hasScope` and `missingScopes` for fine-grained checks.
### hasScope
Returns whether a given scope string is included on the current token.
### missingScopes
Returns scopes your app requested (via provider configuration) that are not yet granted on the current token — useful for prompting re-consent.
## Auth Methods
### signIn
Function to sign a user in via the Basic OAuth flow. It will redirect the user to the Basic login page, and then redirect them back to the page they were on.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { signIn } = useBasic()
return (
<>
>
)
}
```
`signin` (lowercase) is a deprecated alias for `signIn` and will be removed in a future version.
### signInWithHandle
Starts sign-in using a user handle (resolved to a DID server-side). Useful for handle-based login flows in addition to the default `signIn()` redirect.
### signOut
Function to sign a user out. User remains on your page after signing out, but `isSignedIn` will be set to false until they sign in again.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { signOut } = useBasic()
return (
<>
>
)
}
```
`signout` (lowercase) is a deprecated alias for `signOut` and will be removed in a future version.
### signInWithCode
Function to complete authentication using an OAuth authorization code. Useful for custom OAuth flows or React Native apps where you handle the redirect manually.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { signInWithCode } = useBasic()
const handleOAuthCallback = async (code: string, state: string) => {
const result = await signInWithCode(code, state)
if (result.success) {
console.log('Signed in successfully!')
} else {
console.error('Sign in failed:', result.error)
}
}
return <>{/* OAuth callback handling */}>
}
```
**AuthResult type:**
```typescript theme={null}
type AuthResult = {
success: boolean
error?: string
code?: string
}
```
### getToken
Fetches the user's JWT access token. The token is automatically refreshed when needed. Pass `{ forceRefresh: true }` to obtain a new token from the server even if the current one is still valid. Use this for authenticated calls to your backend or the Basic APIs.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { getToken } = useBasic()
const fetchFromMyAPI = async () => {
const token = await getToken()
const response = await fetch('/api/my-endpoint', {
headers: { 'Authorization': `Bearer ${token}` }
})
return response.json()
}
return (
<>
>
)
}
```
### getSignInUrl
Returns the OAuth sign-in URL without redirecting. Useful for custom sign-in flows, React Native apps, or opening in a popup/modal.
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { getSignInUrl } = useBasic()
const handleCustomSignIn = async () => {
const url = await getSignInUrl()
// Open in popup, new tab, or handle as needed
window.open(url, '_blank')
}
return
}
```
`getSignInLink` is a deprecated alias for `getSignInUrl` and will be removed in a future version.
## Development & schema status
### devInfo
Snapshot comparing your local `basic.config` schema to the project schema on the server (project id, version, validity, last check). Intended for debugging and the dev toolbar; `null` if no schema is configured on the provider.
### refreshSchemaStatus
Re-runs the remote schema check and updates `devInfo`. Used by the dev toolbar and custom diagnostics UIs.
## Database
### db
The database object, which you can use to [read, add, update, and delete items](/sdk-reference/react-db).
### dbStatus
The current status of the database connection (string enum). Possible values include:
* `'LOADING'` — Initializing
* `'CONNECTING'` — Connecting to the sync server
* `'ONLINE'` — Connected and syncing
* `'SYNCING'` — Actively syncing
* `'OFFLINE'` — No network (local data may still be available in sync mode)
* `'ERROR_WILL_RETRY'` — Non-fatal error; will retry
* `'ERROR_TOKEN_EXPIRED'` — Auth token expired for sync; refresh or sign in again
* `'ERROR'` — Fatal error
```tsx App.tsx theme={null}
import { useBasic } from '@basictech/react'
function App() {
const { dbStatus } = useBasic()
return (
)
}
```
### dbMode
The current database mode. See [Database Modes](/basic-nextjs/nextjs-modes) for details.
* `'sync'` - Local-first with IndexedDB + real-time WebSocket sync (default)
* `'remote'` - Direct REST API calls to server
***
# useQuery
The `useQuery` hook is used to "subscribe" to data from the database, so that your component automatically re-renders when the data changes. It enables real-time sync between the local database, users' devices, and all other users connected to the same database.
`useQuery` only works in sync mode. In remote mode, use regular async/await patterns instead.
We use it primarily to wrap our `.get()` and `.getAll()` functions, and it takes a function as an argument.
```tsx theme={null}
import { useBasic, useQuery } from '@basictech/react'
function App() {
const { db } = useBasic()
// Example 1: Fetch a single item from the table
const item = useQuery(() => db.collection('tablename').get('ID_OF_ITEM'))
// Example 2: Fetch all items from the table
const items = useQuery(() => db.collection('tablename').getAll())
return (
// render items
)
}
```
The default value is an empty array `[]` while data is loading. This may change in a future version.