Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9a1bec6
admin-dashboard: schema for composio apps integration
CarmenDou May 26, 2026
2420de1
admin-dashboard: apps-connect edge function for composio oauth initiate
CarmenDou May 26, 2026
103fa92
admin-dashboard: apps-poll edge function for composio status + persist
CarmenDou May 26, 2026
05d4710
admin-dashboard: apps-disconnect edge function for composio revoke
CarmenDou May 26, 2026
1bd3f67
admin-dashboard: replace apps toggle stub with composio + native split
CarmenDou May 26, 2026
1908aaa
admin-dashboard: document composio apps setup
CarmenDou May 26, 2026
9de0834
admin-dashboard: restore Apps sidebar entry now that integrations are…
CarmenDou May 26, 2026
38816cd
admin-dashboard: graceful fallback when composio is not configured
CarmenDou Jun 4, 2026
f32a271
admin-dashboard: send-to-slack action on tasks
CarmenDou Jun 4, 2026
60ceed4
admin-dashboard: align composio calls with v3 link endpoint and POST …
CarmenDou Jun 4, 2026
0128af7
admin-dashboard: add google icon to oauth sign-in buttons
CarmenDou Jun 4, 2026
91165f1
admin-dashboard: trim apps catalog to composio-only and refresh registry
CarmenDou Jun 4, 2026
94210e0
admin-dashboard: stop tracking superpowers spec/plan docs
CarmenDou Jun 4, 2026
3322819
Merge remote-tracking branch 'origin/main' into feat/admin-dashboard-…
CarmenDou Jun 4, 2026
e5d7e77
admin-dashboard: address cubic review feedback
CarmenDou Jun 4, 2026
d8f4c7b
admin-dashboard: restore capitalized feature labels, add composio int…
CarmenDou Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admin-dashboard/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ dist-ssr
# Build
*.tsbuildinfo
.tmp
docs/superpowers
49 changes: 47 additions & 2 deletions admin-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ A polished, end-to-end admin dashboard starter built with Vite, TanStack Router/

Demo: [admindashboard.insforge.site](https://admindashboard.insforge.site)

Sign up or sign in to get a personal workspace, then explore. Every page is hooked to live InsForge data — there is no mock layer.
Sign up or sign in to get a personal workspace, then explore. Every page is hooked to live InsForge data. The Apps page lists seven Composio-backed integrations (Slack, GitHub, Notion, Discord, Figma, Linear, Vercel) — once Composio is provisioned (see [Connecting third-party apps](#connecting-third-party-apps)) you can OAuth into the chosen workspace and, from `/tasks`, share any row to a Slack channel with two clicks.

---

Expand Down Expand Up @@ -142,11 +142,56 @@ admin-dashboard/

---

## Connecting third-party apps

The `/apps` page lists seven Composio-backed integrations (Slack, GitHub, Notion, Discord, Figma, Linear, Vercel), each connected per workspace via Composio's hosted OAuth.

### Default behavior without Composio

Out of the box, the `/apps` page renders all seven cards with a disabled **Connect** button and a "Coming soon" label. A banner at the top points to this section. Follow the steps below to switch any of the seven on.

The detection is per-toolkit: only the ones whose `COMPOSIO_AUTH_CONFIG_*` secret you provision become connectable. The rest stay disabled.

### One-time Composio setup

1. Sign up at [composio.dev](https://composio.dev) and create an API key in **Settings → API Keys**.
2. For each of the 7 toolkits (`github`, `notion`, `slack`, `discord`, `figma`, `linear`, `vercel`), open the **Toolkits** page, click the toolkit, then **Create Auth Config** with OAuth 2.0. Copy the resulting `auth_config_id` (`ac_…`).
3. Store every value in InsForge secrets:

```bash
npx @insforge/cli secrets add COMPOSIO_API_KEY <your-api-key>
npx @insforge/cli secrets add COMPOSIO_AUTH_CONFIG_GITHUB ac_xxx
npx @insforge/cli secrets add COMPOSIO_AUTH_CONFIG_NOTION ac_xxx
npx @insforge/cli secrets add COMPOSIO_AUTH_CONFIG_SLACK ac_xxx
npx @insforge/cli secrets add COMPOSIO_AUTH_CONFIG_DISCORD ac_xxx
npx @insforge/cli secrets add COMPOSIO_AUTH_CONFIG_FIGMA ac_xxx
npx @insforge/cli secrets add COMPOSIO_AUTH_CONFIG_LINEAR ac_xxx
npx @insforge/cli secrets add COMPOSIO_AUTH_CONFIG_VERCEL ac_xxx
```

4. Deploy the six edge functions (already shipped under `functions/`):

```bash
npx @insforge/cli functions deploy apps-config --file ./functions/apps-config.ts
npx @insforge/cli functions deploy apps-connect --file ./functions/apps-connect.ts
npx @insforge/cli functions deploy apps-poll --file ./functions/apps-poll.ts
npx @insforge/cli functions deploy apps-disconnect --file ./functions/apps-disconnect.ts
npx @insforge/cli functions deploy apps-slack-list-channels --file ./functions/apps-slack-list-channels.ts
npx @insforge/cli functions deploy apps-slack-send-task --file ./functions/apps-slack-send-task.ts
```

Composio routes its OAuth callback to its own domain — you do **not** need to add any URLs to `insforge.toml`. You do need to allow popups in your browser; the connect flow opens one synchronously when the user clicks **Connect**.

Connections are scoped per workspace: the same workspace user_id is passed to Composio so any member of that workspace can disconnect or replace the connection.

---

## Customization

- **Rebrand** — swap the app name, colors, and icon set. Tailwind CSS variables in `src/styles/globals.css` drive the entire palette; chart colors live there too.
- **Add a page** — drop a new file under `src/routes/_authenticated/` and add a sidebar entry in `src/components/layout/sidebar-nav.ts`. TanStack Router picks it up on next build.
- **Wire a real integration in Apps** — replace the toggle handler in `src/features/apps/use-toggle-app.ts` with an OAuth flow. The mock pattern keeps `app_connections.status` for free.
- **Add or change Apps integrations** — see [Connecting third-party apps](#connecting-third-party-apps) above. Add a new card by inserting a row into `apps_catalog` with the matching `composio_toolkit_slug`, then add the corresponding `COMPOSIO_AUTH_CONFIG_<TOOLKIT>` secret.
- **Send-to-Slack actions** — `functions/apps-slack-send-task.ts` formats and pushes a task to a chosen channel. Mirror the pattern to wire other outbound actions (post to Discord, open a GitHub issue, etc.) on top of Composio's tool execute API.
- **Email invitations** — V1 produces a copyable link. To email instead, wire `insforge.emails.send()` in `src/features/users/use-invitations.ts`.

---
Expand Down
51 changes: 51 additions & 0 deletions admin-dashboard/functions/apps-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createClient } from 'npm:@insforge/sdk'

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

const json = (status: number, body: unknown) =>
new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})

const err = (status: number, code: string, detail?: string) =>
json(status, { error: code, detail })

const COMPOSIO_TOOLKITS = [
'github',
'notion',
'slack',
'discord',
'figma',
'linear',
'vercel',
] as const

export default async function handler(req: Request): Promise<Response> {
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders })
}
if (req.method !== 'GET') return err(405, 'method_not_allowed')

const authHeader = req.headers.get('Authorization')
const userToken = authHeader ? authHeader.replace('Bearer ', '') : null
if (!userToken) return err(401, 'unauthorized')

const client = createClient({
baseUrl: Deno.env.get('INSFORGE_BASE_URL'),
edgeFunctionToken: userToken,
})
const { data: userData } = await client.auth.getCurrentUser()
if (!userData?.user?.id) return err(401, 'unauthorized')

const composio_enabled = !!Deno.env.get('COMPOSIO_API_KEY')
const configured_toolkits = COMPOSIO_TOOLKITS.filter(
(slug) => !!Deno.env.get(`COMPOSIO_AUTH_CONFIG_${slug.toUpperCase()}`),
)

return json(200, { composio_enabled, configured_toolkits })
}
91 changes: 91 additions & 0 deletions admin-dashboard/functions/apps-connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { createClient } from 'npm:@insforge/sdk'

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

const json = (status: number, body: unknown) =>
new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})

const err = (status: number, code: string, detail?: string) =>
json(status, { error: code, detail })

export default async function handler(req: Request): Promise<Response> {
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders })
}
if (req.method !== 'POST') return err(405, 'method_not_allowed')

const authHeader = req.headers.get('Authorization')
const userToken = authHeader ? authHeader.replace('Bearer ', '') : null
if (!userToken) return err(401, 'unauthorized')

const client = createClient({
baseUrl: Deno.env.get('INSFORGE_BASE_URL'),
edgeFunctionToken: userToken,
})

const { data: userData } = await client.auth.getCurrentUser()
if (!userData?.user?.id) return err(401, 'unauthorized')

let body: { app_slug?: string; workspace_id?: string }
try {
body = await req.json()
} catch {
return err(400, 'invalid_json')
}
const { app_slug, workspace_id } = body
if (!app_slug || !workspace_id) return err(400, 'missing_fields')

const { data: appRow, error: appErr } = await client.database
.from('apps_catalog')
.select('integration_kind, composio_toolkit_slug')
.eq('slug', app_slug)
.single()
if (appErr || !appRow) return err(404, 'app_not_found', appErr?.message)
if (appRow.integration_kind !== 'composio' || !appRow.composio_toolkit_slug) {
return err(400, 'not_a_composio_app')
}

const authConfigId = Deno.env.get(
`COMPOSIO_AUTH_CONFIG_${appRow.composio_toolkit_slug.toUpperCase()}`,
)
if (!authConfigId) return err(500, 'auth_config_not_provisioned')

const composioApiKey = Deno.env.get('COMPOSIO_API_KEY')
if (!composioApiKey) return err(500, 'composio_api_key_missing')

const linkRes = await fetch(
'https://backend.composio.dev/api/v3/connected_accounts/link',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': composioApiKey,
},
body: JSON.stringify({
auth_config_id: authConfigId,
user_id: workspace_id,
}),
},
)

if (!linkRes.ok) {
const text = await linkRes.text()
return err(502, 'composio_link_failed', text)
}
const link = (await linkRes.json()) as {
connected_account_id: string
redirect_url: string
}

return json(200, {
request_id: link.connected_account_id,
redirect_url: link.redirect_url,
})
}
82 changes: 82 additions & 0 deletions admin-dashboard/functions/apps-disconnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createClient } from 'npm:@insforge/sdk'

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

const json = (status: number, body: unknown) =>
new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})

const err = (status: number, code: string, detail?: string) =>
json(status, { error: code, detail })

export default async function handler(req: Request): Promise<Response> {
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders })
}
if (req.method !== 'POST') return err(405, 'method_not_allowed')

const authHeader = req.headers.get('Authorization')
const userToken = authHeader ? authHeader.replace('Bearer ', '') : null
if (!userToken) return err(401, 'unauthorized')

const client = createClient({
baseUrl: Deno.env.get('INSFORGE_BASE_URL'),
edgeFunctionToken: userToken,
})

const { data: userData } = await client.auth.getCurrentUser()
if (!userData?.user?.id) return err(401, 'unauthorized')

let body: { app_slug?: string; workspace_id?: string }
try {
body = await req.json()
} catch {
return err(400, 'invalid_json')
}
const { app_slug, workspace_id } = body
if (!app_slug || !workspace_id) return err(400, 'missing_fields')

const { data: existing } = await client.database
.from('app_connections')
.select('config_json')
.eq('workspace_id', workspace_id)
.eq('app_slug', app_slug)
.maybeSingle()

const composioId = (existing?.config_json as Record<string, unknown> | null | undefined)
?.connected_account_id as string | undefined

if (composioId) {
const composioApiKey = Deno.env.get('COMPOSIO_API_KEY')
if (composioApiKey) {
try {
const revokeRes = await fetch(
`https://backend.composio.dev/api/v3/connected_accounts/${encodeURIComponent(composioId)}`,
{ method: 'DELETE', headers: { 'x-api-key': composioApiKey } },
)
if (!revokeRes.ok && revokeRes.status !== 404) {
console.warn(
`composio revoke failed: ${composioId} status=${revokeRes.status} body=${await revokeRes.text()}`,
)
}
} catch (e) {
console.warn(`composio revoke threw: ${composioId} err=${(e as Error).message}`)
}
}
}

const { error: delErr } = await client.database
.from('app_connections')
.delete()
.eq('workspace_id', workspace_id)
.eq('app_slug', app_slug)
if (delErr) return err(500, 'db_delete_failed', delErr.message)

return json(200, { ok: true })
}
Loading
Loading