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
8 changes: 8 additions & 0 deletions packages/cli/deno-runtime/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* - BASE44_APP_ID: App identifier from .app.jsonc
* - BASE44_ACCESS_TOKEN: User's access token
* - BASE44_APP_BASE_URL: App's published URL / subdomain (used for function calls)
* - BASE44_ADMIN: When "true", adds X-Bypass-RLS header for admin access
*/

export {};
Expand All @@ -17,6 +18,8 @@ const scriptPath = Deno.env.get("SCRIPT_PATH");
const appId = Deno.env.get("BASE44_APP_ID");
const accessToken = Deno.env.get("BASE44_ACCESS_TOKEN");
const appBaseUrl = Deno.env.get("BASE44_APP_BASE_URL");
const isAdmin = Deno.env.get("BASE44_ADMIN") === "true";
const dataEnv = Deno.env.get("BASE44_DATA_ENV");

if (!scriptPath) {
console.error("SCRIPT_PATH environment variable is required");
Expand All @@ -35,10 +38,15 @@ if (!appBaseUrl) {

import { createClient } from "npm:@base44/sdk";

const customHeaders: Record<string, string> = {};
if (isAdmin) customHeaders["X-Bypass-RLS"] = "true";
if (dataEnv) customHeaders["X-Data-Env"] = dataEnv;

const base44 = createClient({
appId,
token: accessToken,
serverUrl: appBaseUrl,
headers: customHeaders,
});

(globalThis as any).base44 = base44;
Expand Down
32 changes: 27 additions & 5 deletions packages/cli/src/cli/commands/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function readStdin(): Promise<string> {
}

async function execAction(
options: { admin?: boolean; env?: string },
isNonInteractive: boolean,
): Promise<RunCommandResult> {
const noInputError = new InvalidInputError(
Expand All @@ -43,7 +44,12 @@ async function execAction(
throw noInputError;
}

const { exitCode } = await runScript({ appId: getAppConfig().id, code });
const { exitCode } = await runScript({
appId: getAppConfig().id,
code,
admin: options.admin,
dataEnv: options.env,
});

if (exitCode !== 0) {
process.exitCode = exitCode;
Expand All @@ -57,6 +63,14 @@ export function getExecCommand(): Command {
.description(
"Run a script with the Base44 SDK pre-authenticated as the current user",
)
.option(
"--admin",
"Run with admin privileges (bypass RLS). Requires app owner/editor role.",
)
Comment on lines +66 to +69
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should be specific and call this --skip-rls or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, just thinking how not to "scare" users /agetns with such a name to the flag. @claude got a good idea for a flag name?

Copy link

@claude claude bot Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @netanelgilad's task in 21s —— View job


Here are a few flag name ideas that convey elevated access without being alarming:

Name Pros Cons
--privileged Familiar from Docker/containers, clearly signals elevated access Still sounds "serious"
--full-access Plain English, self-explanatory A bit verbose
--unrestricted Describes exactly what it does (no RLS restrictions) Could imply security risk
--as-owner Communicates who you're acting as, not what you're bypassing Slightly misleading (editor role also works)
--bypass-filters Softer than "bypass-rls", avoids technical jargon Vague

My recommendation: --privileged

It's already familiar to developers from tools like Docker (docker run --privileged), signals that the user is opting into elevated permissions intentionally, and is less alarming than --admin or --skip-rls. It also doesn't expose the internal "RLS" terminology to users who may not know what that means.

Runner-up: --full-access — if you want something even more self-documenting for non-technical users or agents reading the help text.

Copy link
Contributor Author

@netanelgilad netanelgilad Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kfirstri I don't have any strong opinion here, got something you liked here?

.option(
"--env <environment>",
"Data environment to use (dev, share, or production). Defaults to production.",
)
Comment on lines +70 to +73
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this? is this a documented feature? for example with is dev vs shared?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.addHelpText(
"after",
`
Expand All @@ -65,9 +79,17 @@ Examples:
$ cat ./script.ts | base44 exec

Inline script:
$ echo "const users = await base44.entities.User.list()" | base44 exec`,
$ echo "const users = await base44.entities.User.list()" | base44 exec

Run with admin privileges (bypass RLS):
$ echo "const all = await base44.entities.Task.list()" | base44 exec --admin`,
)
.action(async (_options: unknown, command: Base44Command) => {
return await execAction(command.isNonInteractive);
});
.action(
async (
options: { admin?: boolean; env?: string },
command: Base44Command,
) => {
return await execAction(options, command.isNonInteractive);
},
);
}
6 changes: 5 additions & 1 deletion packages/cli/src/core/exec/run-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { verifyDenoInstalled } from "@/core/utils/index.js";
interface RunScriptOptions {
appId: string;
code: string;
admin?: boolean;
dataEnv?: string;
}

interface RunScriptResult {
Expand All @@ -17,7 +19,7 @@ interface RunScriptResult {
export async function runScript(
options: RunScriptOptions,
): Promise<RunScriptResult> {
const { appId, code } = options;
const { appId, code, admin, dataEnv } = options;

verifyDenoInstalled("to run scripts with exec");

Expand Down Expand Up @@ -53,6 +55,8 @@ export async function runScript(
BASE44_APP_ID: appId,
BASE44_ACCESS_TOKEN: appUserToken,
BASE44_APP_BASE_URL: appBaseUrl,
...(admin ? { BASE44_ADMIN: "true" } : {}),
...(dataEnv ? { BASE44_DATA_ENV: dataEnv } : {}),
},
stdio: "inherit",
},
Expand Down
171 changes: 171 additions & 0 deletions packages/cli/tests/cli/exec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,175 @@ describe("exec command", () => {

expect(result.exitCode).toBe(42);
});

describe("--admin flag", () => {
it("sends X-Bypass-RLS header when --admin is passed", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: t.api.baseUrl });

let capturedHeaders: Record<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
res.json([]);
},
);

t.givenStdin("await base44.entities.Task.list();");

const result = await t.run("exec", "--admin");

t.expectResult(result).toSucceed();
expect(capturedHeaders["x-bypass-rls"]).toBe("true");
});

it("does NOT send X-Bypass-RLS header without --admin", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: t.api.baseUrl });

let capturedHeaders: Record<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
res.json([]);
},
);

t.givenStdin("await base44.entities.Task.list();");

const result = await t.run("exec");

t.expectResult(result).toSucceed();
expect(capturedHeaders["x-bypass-rls"]).toBeUndefined();
});
});

describe("--env flag", () => {
it("sends X-Data-Env header with value 'dev' when --env dev is passed", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: t.api.baseUrl });

let capturedHeaders: Record<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
res.json([]);
},
);

t.givenStdin("await base44.entities.Task.list();");

const result = await t.run("exec", "--env", "dev");

t.expectResult(result).toSucceed();
expect(capturedHeaders["x-data-env"]).toBe("dev");
});

it("sends X-Data-Env header with value 'prod' when --env prod is passed", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: t.api.baseUrl });

let capturedHeaders: Record<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
res.json([]);
},
);

t.givenStdin("await base44.entities.Task.list();");

const result = await t.run("exec", "--env", "prod");

t.expectResult(result).toSucceed();
expect(capturedHeaders["x-data-env"]).toBe("prod");
});

it("does NOT send X-Data-Env header without --env", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: t.api.baseUrl });

let capturedHeaders: Record<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
res.json([]);
},
);

t.givenStdin("await base44.entities.Task.list();");

const result = await t.run("exec");

t.expectResult(result).toSucceed();
expect(capturedHeaders["x-data-env"]).toBeUndefined();
});
});

describe("--admin and --env combined", () => {
it("sends both X-Bypass-RLS and X-Data-Env headers when both flags are passed", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: t.api.baseUrl });

let capturedHeaders: Record<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
res.json([]);
},
);

t.givenStdin("await base44.entities.Task.list();");

const result = await t.run("exec", "--admin", "--env", "dev");

t.expectResult(result).toSucceed();
expect(capturedHeaders["x-bypass-rls"]).toBe("true");
expect(capturedHeaders["x-data-env"]).toBe("dev");
});
});

describe("env vars passed to Deno subprocess", () => {
it("passes BASE44_ADMIN env var when --admin is used", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: "https://test-app.base44.app" });
t.givenStdin('console.log("ADMIN=" + Deno.env.get("BASE44_ADMIN"));');

const result = await t.run("exec", "--admin");

t.expectResult(result).toSucceed();
expect(result.stdout).toContain("ADMIN=true");
});

it("passes BASE44_DATA_ENV env var when --env is used", async () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAuthToken("test-app-token");
t.api.mockSiteUrl({ url: "https://test-app.base44.app" });
t.givenStdin('console.log("ENV=" + Deno.env.get("BASE44_DATA_ENV"));');

const result = await t.run("exec", "--env", "dev");

t.expectResult(result).toSucceed();
expect(result.stdout).toContain("ENV=dev");
});
});
});
Loading