Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
68 changes: 52 additions & 16 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,38 +1,73 @@


# =============================================================================
# ROOT SUPERSET ENV
# Shared by all worktrees via direnv
# SUPERSET LOCAL DEVELOPMENT ENV
# Copied to .env by `bun setup:local`.
#
# Keep .env machine-local and untracked. Blank optional integration keys are
# intentional in the local profile; the app should boot without third-party
# credentials and degrade only when those specific features are exercised.
# =============================================================================

# -----------------------------------------------------------------------------
# Neon Organization Credentials
# Local Contributor Profile
# -----------------------------------------------------------------------------
NEON_ORG_ID=
NEON_PROJECT_ID=
NEON_API_KEY=
# Required for fresh-clone local development without third-party credentials.
SUPERSET_PROFILE=local

# Keep desktop dev state separate from production / canary installs.
# `bun setup:local` rewrites this to local-dev-<worktree>.
SUPERSET_WORKSPACE_NAME=local-dev

# -----------------------------------------------------------------------------
# Local Database
# -----------------------------------------------------------------------------
# `bun setup:local` rewrites these to a database name derived from the
# worktree path, so multiple local worktrees can share one Postgres container
# without sharing schema/auth state.
SUPERSET_LOCAL_DATABASE_NAME=superset
DATABASE_URL=postgres://superset:superset@localhost:5433/superset
DATABASE_URL_UNPOOLED=postgres://superset:superset@localhost:5433/superset

# -----------------------------------------------------------------------------
# Neon Database Credentials (Production)
# Local Ports
# -----------------------------------------------------------------------------
DATABASE_URL=
DATABASE_URL_UNPOOLED=
WEB_PORT=4640
API_PORT=4641
DESKTOP_VITE_PORT=4645
DESKTOP_NOTIFICATIONS_PORT=4646
ELECTRIC_PORT=4649
CADDY_ELECTRIC_PORT=4650
WRANGLER_PORT=4652

# -----------------------------------------------------------------------------
# Cross-App URLs (Local Dev)
# Local App URLs
# -----------------------------------------------------------------------------
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_WEB_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:4641
NEXT_PUBLIC_WEB_URL=http://localhost:4640
NEXT_PUBLIC_ADMIN_URL=http://localhost:3003
NEXT_PUBLIC_MARKETING_URL=http://localhost:3002
NEXT_PUBLIC_DOCS_URL=http://localhost:3004
NEXT_PUBLIC_ELECTRIC_URL=https://localhost:4650

# -----------------------------------------------------------------------------
# Better Auth
# Local Auth Defaults
# -----------------------------------------------------------------------------
BETTER_AUTH_SECRET=
BETTER_AUTH_SECRET=local_dev_secret_not_for_production
NEXT_PUBLIC_COOKIE_DOMAIN=localhost

# =============================================================================
# OPTIONAL INTEGRATION CREDENTIALS
# Leave these blank for the default local contributor flow.
# =============================================================================

# -----------------------------------------------------------------------------
# Neon Organization Credentials
# -----------------------------------------------------------------------------
# Only needed for Neon-backed/internal workflows. The local setup script uses the
# Docker Postgres URLs above.
NEON_ORG_ID=
NEON_PROJECT_ID=
NEON_API_KEY=

# -----------------------------------------------------------------------------
# OAuth Credentials (for Desktop App direct auth)
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -99,6 +134,7 @@ KV_REST_API_TOKEN=

# QStash API token (used for background job scheduling in packages/trpc and apps/api)
QSTASH_TOKEN=
QSTASH_URL=

# QStash webhook signature verification keys (used to verify webhook requests)
QSTASH_CURRENT_SIGNING_KEY=
Expand Down
6 changes: 3 additions & 3 deletions Caddyfile.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
# Avoids browser's 6-connection limit when using 10+ Electric streams.
#
# Copy to Caddyfile: cp Caddyfile.example Caddyfile
# Install caddy: brew install caddy (macOS) / see https://caddyserver.com/docs/install
# Install caddy: brew install caddy

{
auto_https disable_redirects
}

https://localhost:3010 {
reverse_proxy localhost:3001 {
https://localhost:{$CADDY_ELECTRIC_PORT} {
reverse_proxy localhost:{$WRANGLER_PORT} {
flush_interval -1
}
}
46 changes: 8 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ If it runs in a terminal, it runs on Superset

| Requirement | Details |
|:------------|:--------|
| **OS** | macOS (Windows/Linux untested) |
| **OS** | macOS |
| **Runtime** | [Bun](https://bun.sh/) v1.0+ |
| **Version Control** | Git 2.20+ |
| **GitHub CLI** | [gh](https://cli.github.com/) |
Expand All @@ -85,57 +85,27 @@ If it runs in a terminal, it runs on Superset

### Build from Source

<details>
<summary>Click to expand build instructions</summary>
For a complete contributor workflow that boots a fresh clone with no third-party credentials (Neon / OAuth / Stripe / Resend keys are all optional), follow **[Local Development](docs/LOCAL_DEVELOPMENT.md)**.

**1. Clone the repository**
Short version:

```bash
git clone https://github.com/superset-sh/superset.git
cd superset
```

**2. Set up environment variables** (choose one):

Option A: Full setup
```bash
cp .env.example .env
# Edit .env and fill in the values
```

Option B: Skip env validation (for quick local testing)
```bash
cp .env.example .env
echo 'SKIP_ENV_VALIDATION=1' >> .env
```

**3. Set up Caddy** (reverse proxy for Electric SQL streams):

```bash
# Install caddy: brew install caddy (macOS) or see https://caddyserver.com/docs/install
cp Caddyfile.example Caddyfile

# Without this, Chromium rejects https://localhost:* with ERR_CERT_AUTHORITY_INVALID.
# Prompts for sudo once.
caddy trust
```

**4. Install dependencies and run**

```bash
bun install
bun run dev
bun setup:local
bun dev
```

**5. Build the desktop app**
`bun setup:local` copies local examples, assigns this worktree its own local database and desktop state, starts Docker Postgres/Electric, trusts Caddy's local CA, and runs migrations. It does not overwrite internal `.env` files. The desktop window opens auto-signed-in as `admin@local.test`, with state isolated under `~/.superset-local-dev-*` so source builds do not reuse production or canary desktop state. See [Local Development](docs/LOCAL_DEVELOPMENT.md) for details and troubleshooting.

To build a distributable desktop app:

```bash
bun run build
open apps/desktop/release
```

</details>

## Keyboard Shortcuts

All shortcuts are customizable via **Settings > Keyboard Shortcuts** (`⌘/`). See [full documentation](https://docs.superset.sh/keyboard-shortcuts).
Expand Down
5 changes: 4 additions & 1 deletion apps/admin/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { shouldSkipEnvValidation } from "@superset/shared/deployment-profile";
import { createEnv } from "@t3-oss/env-nextjs";
import { vercel } from "@t3-oss/env-nextjs/presets-zod";
import { z } from "zod";

const skipValidation = shouldSkipEnvValidation();

export const env = createEnv({
extends: [vercel()],
shared: {
Expand Down Expand Up @@ -49,6 +52,6 @@ export const env = createEnv({
NEXT_PUBLIC_SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
},

skipValidation: !!process.env.SKIP_ENV_VALIDATION,
skipValidation,
emptyStringAsUndefined: true,
});
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"clean": "git clean -xdf .cache .next .turbo node_modules",
"dev": "dotenv -e ../../.env -- sh -c 'exec next dev --port ${API_PORT:-3001}'",
"start": "next start --port 3001",
"test": "bun test",
"typecheck": "tsc --noEmit"
},
"dependencies": {
Expand Down
14 changes: 9 additions & 5 deletions apps/api/src/app/api/automations/evaluate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import { dbWs } from "@superset/db/client";
import { automations, subscriptions } from "@superset/db/schema";
import { ACTIVE_SUBSCRIPTION_STATUSES } from "@superset/shared/billing";
import { nextOccurrenceAfter } from "@superset/shared/rrule";
import { Client, Receiver } from "@upstash/qstash";
import { Receiver } from "@upstash/qstash";
import { and, eq, exists, inArray, lte, ne, sql } from "drizzle-orm";

import { env } from "@/env";
import { qstash } from "@/lib/qstash";

export const dynamic = "force-dynamic";

const qstash = new Client({
token: env.QSTASH_TOKEN,
baseUrl: env.QSTASH_URL,
});
const receiver = new Receiver({
currentSigningKey: env.QSTASH_CURRENT_SIGNING_KEY,
nextSigningKey: env.QSTASH_NEXT_SIGNING_KEY,
Expand Down Expand Up @@ -73,6 +70,13 @@ export async function POST(request: Request): Promise<Response> {
return Response.json({ enqueued: 0 });
}

if (!qstash) {
return Response.json(
{ error: "QSTASH_TOKEN is required to enqueue automation jobs" },
{ status: 503 },
);
}

await qstash.batchJSON(
due.map((automation) => {
const scheduledFor = bucketToMinute(automation.nextRunAt);
Expand Down
34 changes: 23 additions & 11 deletions apps/api/src/app/api/github/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { db } from "@superset/db/client";
import { githubInstallations, members } from "@superset/db/schema";
import { Client } from "@upstash/qstash";
import { and, eq } from "drizzle-orm";

import { env } from "@/env";
import { verifySignedState } from "@/lib/oauth-state";
import { qstash, requireQstash } from "@/lib/qstash";
import { githubApp } from "../octokit";

const qstash = new Client({ token: env.QSTASH_TOKEN });

/**
* Callback handler for GitHub App installation.
* GitHub redirects here after the user installs/configures the app.
Expand Down Expand Up @@ -122,14 +120,28 @@ export async function GET(request: Request) {

// Queue initial sync job
try {
await qstash.publishJSON({
url: `${env.NEXT_PUBLIC_API_URL}/api/github/jobs/initial-sync`,
body: {
installationDbId: savedInstallation.id,
organizationId,
},
retries: 3,
});
const syncUrl = `${env.NEXT_PUBLIC_API_URL}/api/github/jobs/initial-sync`;
const syncBody = {
installationDbId: savedInstallation.id,
organizationId,
};

if (env.NODE_ENV === "development" && !qstash) {
const response = await fetch(syncUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(syncBody),
});
if (!response.ok) {
throw new Error(`Local sync request failed: ${response.status}`);
}
} else {
await requireQstash("github/callback").publishJSON({
url: syncUrl,
body: syncBody,
retries: 3,
});
}
} catch (error) {
console.error(
"[github/callback] Failed to queue initial sync job:",
Expand Down
19 changes: 19 additions & 0 deletions apps/api/src/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
getDeploymentProfile,
isStrictProfile,
} from "@superset/shared/deployment-profile";
import { NextResponse } from "next/server";
import { getIntegrationStatuses } from "../../../lib/integration-status";

export function GET() {
const profile = getDeploymentProfile();
if (isStrictProfile(profile)) {
return NextResponse.json({ ok: true });
}

return NextResponse.json({
ok: true,
profile,
Comment thread
Kitenite marked this conversation as resolved.
integrations: getIntegrationStatuses(),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
28 changes: 20 additions & 8 deletions apps/api/src/app/api/integrations/linear/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ import { LinearClient } from "@linear/sdk";
import { db } from "@superset/db/client";
import { integrationConnections, members, users } from "@superset/db/schema";
import { linearTokenResponseSchema } from "@superset/trpc/integrations/linear";
import { Client } from "@upstash/qstash";
import { and, eq, isNull, ne } from "drizzle-orm";

import { env } from "@/env";
import { verifySignedState } from "@/lib/oauth-state";
import { qstash, requireQstash } from "@/lib/qstash";

const UNIQUE_VIOLATION = "23505";
const ACTIVE_LINKAGE_INDEX =
"integration_connections_provider_external_org_active_unique";

const qstash = new Client({ token: env.QSTASH_TOKEN });

export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
Expand Down Expand Up @@ -149,11 +147,25 @@ export async function GET(request: Request) {
}

try {
await qstash.publishJSON({
url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/linear/jobs/initial-sync`,
body: { organizationId, creatorUserId: userId },
retries: 3,
});
const syncUrl = `${env.NEXT_PUBLIC_API_URL}/api/integrations/linear/jobs/initial-sync`;
const syncBody = { organizationId, creatorUserId: userId };

if (env.NODE_ENV === "development" && !qstash) {
const response = await fetch(syncUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(syncBody),
});
if (!response.ok) {
throw new Error(`Local sync request failed: ${response.status}`);
}
} else {
await requireQstash("linear/callback").publishJSON({
url: syncUrl,
body: syncBody,
retries: 3,
});
}
} catch (error) {
console.error("Failed to queue initial sync job:", error);
return Response.redirect(
Expand Down
Loading