# Add account connecting to your site

> Page: https://docs.flung.dev/connect-accounts. Index of every page: https://docs.flung.dev/llms.txt

Put a panel on your own page where your users connect, reconnect and disconnect their social accounts, without your API key ever reaching the browser.

## TL;DR

| Question | Answer |
|---|---|
| What are the parts? | A route on your server that makes a connect token, and `<flung-connect>` on the page, which uses it. |
| What does a person see? | The platforms you offer and their connected accounts, with Connect, Reconnect and Disconnect. Connecting opens the platform's own sign-in in a small window. |
| Which frameworks? | Any. Next.js, React with Vite and a plain page are shown below, each from a working example. |
| Can each of my users have their own accounts? | Yes: make each token with that user's `profileId`. |

## Install

```bash
npm install @flung.dev/sdk @flung.dev/connect
```

## Next.js

The server keeps the key, in `.env.local`:

**.env.local**

```bash
# A full-access Flung API key (flung_sk_...). Server only: never prefix it with NEXT_PUBLIC_.
FLUNG_API_KEY=

# Optional. The API: https://api.flung.dev (the default) or https://api-preview.flung.dev.
FLUNG_BASE_URL=

# The origin this app is served from, which connect tokens are made for. http://localhost:3000 when left out.
APP_ORIGIN=
```

A route makes a connect token for the page. Put your own sign-in check at its top:

**app/api/connect-token/route.ts**

```ts
import { Flung, FlungError } from '@flung.dev/sdk'

/**
 * `POST /api/connect-token`: a short-lived connect token for this app's own page. The API key stays here on the server;
 * the token it makes can only list, connect, reconnect and disconnect accounts, from `APP_ORIGIN`, for fifteen minutes.
 *
 * In a real app, check who is signed in first, and pass their profile as `profileId` so each person sees only theirs.
 */
export async function POST(): Promise<Response> {
  const apiKey = process.env.FLUNG_API_KEY
  if (apiKey === undefined || apiKey === '') return Response.json({ error: 'Set FLUNG_API_KEY in .env.local.' }, { status: 500 })
  const flung = new Flung({ apiKey, ...(process.env.FLUNG_BASE_URL ? { baseUrl: process.env.FLUNG_BASE_URL } : {}) })
  try {
    const { token, expiresAt } = await flung.connectTokens.create({ origins: [process.env.APP_ORIGIN || 'http://localhost:3000'], expiresIn: 900 })
    return Response.json({ token, expiresAt }, { headers: { 'cache-control': 'no-store' } })
  } catch (error) {
    const status = error instanceof FlungError ? (error.status ?? 502) : 502
    return Response.json({ error: error instanceof Error ? error.message : 'Could not make a connect token.' }, { status })
  }
}
```

The page shows the panel, which asks the route for a token, and again when one expires:

**app/accounts.tsx**

```tsx
'use client'

import { useCallback, useState } from 'react'

import { FlungConnect } from '@flung.dev/connect/react'

/** Asks this app's server for a connect token; the panel calls it again when a token expires. */
async function connectToken(): Promise<string> {
  const response = await fetch('/api/connect-token', { method: 'POST' })
  const body = (await response.json()) as { token?: string; error?: string }
  if (!response.ok || body.token === undefined) throw new Error(body.error ?? 'Could not make a connect token.')
  return body.token
}

export function Accounts({ baseUrl }: { baseUrl: string | undefined }) {
  const [events, setEvents] = useState<string[]>([])
  const log = useCallback((line: string) => setEvents((previous) => [line, ...previous].slice(0, 5)), [])
  return (
    <>
      <FlungConnect
        token={connectToken}
        baseUrl={baseUrl}
        onConnect={({ accounts }) => log(`Connected ${accounts.map((account) => account.username ?? account.id).join(', ')}`)}
        onDisconnect={({ account }) => log(`Disconnected ${account.username ?? account.id}`)}
        onError={({ code, message }) => log(`${code}: ${message}`)}
      />
      {events.length > 0 && (
        <ul className="events" aria-label="What happened">
          {events.map((line, index) => (
            <li key={`${index}-${line}`}>{line}</li>
          ))}
        </ul>
      )}
    </>
  )
}
```

## React with Vite

For trying the panel out, Vite's development server can make the token itself:

**vite.config.ts**

```ts
import { Flung } from '@flung.dev/sdk'
import { defineConfig, loadEnv } from 'vite'
import type { Connect, Plugin } from 'vite'

/**
 * `POST /connect-token` on the dev and preview servers: makes a connect token for the page with the key from
 * `.env.local`. The key has no `VITE_` prefix, so Vite never puts it in the bundle. This is for trying the panel out; a
 * deployed app makes tokens on its own server, behind its own sign-in.
 */
function connectTokens(apiKey: string, baseUrl: string | undefined): Plugin {
  const handler: Connect.NextHandleFunction = (request, response, next) => {
    if (request.method !== 'POST' || request.url !== '/connect-token') return next()
    const send = (status: number, body: unknown) => {
      response.statusCode = status
      response.setHeader('content-type', 'application/json')
      response.setHeader('cache-control', 'no-store')
      response.end(JSON.stringify(body))
    }
    if (apiKey === '') return send(500, { error: 'Set FLUNG_API_KEY in .env.local.' })
    const origin = request.headers.origin ?? `http://${request.headers.host ?? 'localhost:5173'}`
    const flung = new Flung({ apiKey, ...(baseUrl ? { baseUrl } : {}) })
    flung.connectTokens.create({ origins: [origin], expiresIn: 900 }).then(
      ({ token }) => send(200, { token }),
      (error: unknown) => send(502, { error: error instanceof Error ? error.message : 'Could not make a connect token.' }),
    )
  }
  return { name: 'flung-connect-tokens', configureServer: (server) => void server.middlewares.use(handler), configurePreviewServer: (server) => void server.middlewares.use(handler) }
}

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')
  return {
    plugins: [connectTokens(env.FLUNG_API_KEY ?? '', env.FLUNG_BASE_URL || undefined)],
    define: { 'import.meta.env.FLUNG_BASE_URL': JSON.stringify(env.FLUNG_BASE_URL || '') },
    esbuild: { jsx: 'automatic' },
  }
})
```

**src/App.tsx**

```tsx
import { useCallback, useState } from 'react'

import { FlungConnect } from '@flung.dev/connect/react'

/** Asks the dev server for a connect token; the panel calls it again when a token expires. */
async function connectToken(): Promise<string> {
  const response = await fetch('/connect-token', { method: 'POST' })
  const body = (await response.json()) as { token?: string; error?: string }
  if (!response.ok || body.token === undefined) throw new Error(body.error ?? 'Could not make a connect token.')
  return body.token
}

const baseUrl = import.meta.env.FLUNG_BASE_URL || undefined

export function App() {
  const [events, setEvents] = useState<string[]>([])
  const log = useCallback((line: string) => setEvents((previous) => [line, ...previous].slice(0, 5)), [])
  return (
    <main>
      <h1>Connect your accounts</h1>
      <p>We post to the accounts you connect here. You can disconnect them at any time.</p>
      <FlungConnect
        token={connectToken}
        baseUrl={baseUrl}
        onConnect={({ accounts }) => log(`Connected ${accounts.map((account) => account.username ?? account.id).join(', ')}`)}
        onDisconnect={({ account }) => log(`Disconnected ${account.username ?? account.id}`)}
        onError={({ code, message }) => log(`${code}: ${message}`)}
      />
      {events.length > 0 && (
        <ul className="events" aria-label="What happened">
          {events.map((line, index) => (
            <li key={`${index}-${line}`}>{line}</li>
          ))}
        </ul>
      )}
    </main>
  )
}
```

A deployed app makes tokens on its own server, behind its own sign-in, as the Next.js route does.

## A plain page

No framework and nothing to install: a server of a few lines makes the token, and the page loads the panel.

**server.mjs**

```js
#!/usr/bin/env node
/**
 * The smallest server a connect panel needs, with nothing to install: it keeps the API key, makes a connect token for
 * the page when the page asks, and serves the page and the built package.
 *
 *   pnpm build
 *   FLUNG_API_KEY=flung_sk_... node examples/vanilla/server.mjs
 *
 * Then open http://localhost:5173. FLUNG_BASE_URL=https://api-preview.flung.dev tries it against preview.
 */
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, join, normalize } from 'node:path'

const PORT = Number(process.env.PORT ?? 5173)
const ORIGIN = `http://localhost:${PORT}`
const BASE_URL = process.env.FLUNG_BASE_URL ?? 'https://api.flung.dev'
const API_KEY = process.env.FLUNG_API_KEY
if (API_KEY === undefined) throw new Error('Set FLUNG_API_KEY to a full-access Flung API key.')

const here = import.meta.dirname
const dist = join(here, '..', '..', 'packages', 'connect', 'dist')
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8' }

/** In your app, check who is signed in here first, and pass their profile as `profileId`. */
async function connectToken() {
  const response = await fetch(`${BASE_URL}/v1/connect-tokens`, {
    method: 'POST',
    headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json' },
    body: JSON.stringify({ origins: [ORIGIN], expiresIn: 900 }),
  })
  return { status: response.status, body: await response.text() }
}

createServer(async (request, response) => {
  const path = new URL(request.url ?? '/', ORIGIN).pathname
  if (request.method === 'POST' && path === '/connect-token') {
    const made = await connectToken()
    response.writeHead(made.status, { 'content-type': 'application/json' }).end(made.body)
    return
  }
  const file = path === '/' ? join(here, 'index.html') : path.startsWith('/connect/') ? normalize(join(dist, path.slice('/connect/'.length))) : null
  if (file === null || (file !== join(here, 'index.html') && !file.startsWith(dist))) {
    response.writeHead(404).end()
    return
  }
  try {
    const page = file.endsWith('index.html') ? (await readFile(file, 'utf8')).replace('__FLUNG_BASE_URL__', BASE_URL) : await readFile(file)
    response.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' }).end(page)
  } catch {
    response.writeHead(404).end()
  }
}).listen(PORT, () => process.stdout.write(`Open ${ORIGIN}\n`))
```

**index.html**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Connect your accounts</title>
    <style>
      body { font: 16px/1.5 system-ui, sans-serif; max-width: 36rem; margin: 8vh auto; padding: 0 16px; }
      @media (prefers-color-scheme: dark) { body { background: #09090b; color: #f4f4f5; } }
      pre { font-size: 13px; overflow: auto; }
    </style>
  </head>
  <body>
    <h1>Connect your accounts</h1>
    <p>Flung posts to the accounts you connect here. You can disconnect them at any time.</p>
    <flung-connect id="panel" base-url="__FLUNG_BASE_URL__"></flung-connect>
    <pre id="log"></pre>
    <script type="module">
      import '/connect/register.js'

      const panel = document.getElementById('panel')
      const log = (type, detail) => (document.getElementById('log').textContent += `${type} ${JSON.stringify(detail)}\n`)
      // A function, so the panel asks for a fresh token when one expires.
      panel.token = async () => {
        const response = await fetch('/connect-token', { method: 'POST' })
        if (!response.ok) throw new Error(`Could not make a connect token: ${await response.text()}`)
        return (await response.json()).token
      }
      for (const type of ['flung-connect', 'flung-disconnect', 'flung-error']) panel.addEventListener(type, (event) => log(type, event.detail))
    </script>
  </body>
</html>
```

## How it behaves

- The panel offers only the platforms this Flung can connect, narrowed by its `platforms` attribute and the token's own list.
- Connecting opens the platform's sign-in in a small window. When a browser blocks the window, the panel shows a link instead.
- Mastodon asks for the person's server first; Bluesky takes a handle when they have one.
- Disconnecting asks to confirm. The account then leaves the panel.
- With `mode="redirect"` the whole page goes to the platform and back, for pages that cannot open windows.

Every attribute, event, style hook and word is in the [connect panel reference](https://docs.flung.dev/reference/connect.md).
