@rebasepro/cli 0.10.0 → 0.10.1-canary.0a881d4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bundle.d.ts +146 -0
- package/dist/commands/apps.d.ts +1 -0
- package/dist/commands/build.d.ts +1 -1
- package/dist/commands/cloud/bundle-deploy.d.ts +53 -0
- package/dist/commands/cloud/context.d.ts +28 -0
- package/dist/commands/cloud/deployments.d.ts +16 -0
- package/dist/commands/cloud/env.d.ts +2 -0
- package/dist/commands/cloud/resources.d.ts +38 -0
- package/dist/commands/generate_sdk.d.ts +12 -0
- package/dist/commands/start.d.ts +1 -1
- package/dist/fold-static.d.ts +46 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +2567 -100
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +83 -0
- package/dist/utils/package-manager.d.ts +26 -2
- package/dist/utils/project.d.ts +11 -3
- package/package.json +8 -7
- package/runtime/dev-server.mjs +43 -0
- package/templates/overlays/baas/backend/src/index.ts +28 -4
- package/templates/overlays/baas/backend/src/storage.ts +71 -0
- package/templates/overlays/baas/rebase.json +14 -0
- package/templates/template/.env.example +16 -3
- package/templates/template/README.md +39 -29
- package/templates/template/backend/src/index.ts +28 -4
- package/templates/template/config/admin.d.ts +9 -0
- package/templates/template/config/collections/authors.ts +11 -9
- package/templates/template/config/collections/posts.ts +6 -4
- package/templates/template/config/collections/presets/ecommerce/categories.ts +7 -5
- package/templates/template/config/collections/presets/ecommerce/orders.ts +32 -20
- package/templates/template/config/collections/presets/ecommerce/products.ts +21 -13
- package/templates/template/config/collections/tags.ts +5 -3
- package/templates/template/config/collections/users.ts +34 -29
- package/templates/template/config/frontend-assets.d.ts +17 -0
- package/templates/template/config/index.ts +6 -0
- package/templates/template/config/package.json +26 -25
- package/templates/template/config/storage.ts +88 -0
- package/templates/template/gitignore +4 -0
- package/templates/template/rebase.json +20 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ManagedCompatibility, RebaseAppConfig, RebaseBackendAppConfig, RebaseProjectManifest } from "@rebasepro/types";
|
|
2
|
+
/** Runtime range written into new manifests. */
|
|
3
|
+
export declare const CURRENT_RUNTIME_RANGE = "^1";
|
|
4
|
+
/** Conventional locations, matching what `rebase init` scaffolds. */
|
|
5
|
+
export declare const DEFAULT_CONFIG_DIR = "config";
|
|
6
|
+
export declare const DEFAULT_FUNCTIONS_DIR = "backend/functions";
|
|
7
|
+
export declare const DEFAULT_CRONS_DIR = "backend/crons";
|
|
8
|
+
export declare const DEFAULT_SCHEMA_FILE = "backend/src/schema.generated.ts";
|
|
9
|
+
export interface ManifestValidationIssue {
|
|
10
|
+
/** Dotted path to the offending value, e.g. `apps.web.output`. */
|
|
11
|
+
path: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LoadedManifest {
|
|
15
|
+
manifest: RebaseProjectManifest;
|
|
16
|
+
/** Where it came from — a real file, or inferred from the directory layout. */
|
|
17
|
+
source: "file" | "synthesized";
|
|
18
|
+
/** Absolute path to `rebase.json`, when one exists. */
|
|
19
|
+
filePath?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class ManifestError extends Error {
|
|
22
|
+
readonly issues: ManifestValidationIssue[];
|
|
23
|
+
constructor(message: string, issues?: ManifestValidationIssue[]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate a parsed manifest, collecting every problem.
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateManifest(raw: unknown): {
|
|
29
|
+
manifest?: RebaseProjectManifest;
|
|
30
|
+
issues: ManifestValidationIssue[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Infer a manifest from a directory that does not have one.
|
|
34
|
+
*
|
|
35
|
+
* This mirrors exactly what the template scaffolds, which is what makes adopting
|
|
36
|
+
* the manifest a no-op for existing projects: the synthesized result is what
|
|
37
|
+
* they would have written by hand.
|
|
38
|
+
*
|
|
39
|
+
* An ejected backend — one with its own `src/index.ts` entrypoint — is reported
|
|
40
|
+
* as a `custom` app rather than a `backend` app. That is not a downgrade; it is
|
|
41
|
+
* an accurate description, and it is what keeps such a project deploying exactly
|
|
42
|
+
* as it does today.
|
|
43
|
+
*/
|
|
44
|
+
export declare function synthesizeManifest(projectRoot: string): RebaseProjectManifest;
|
|
45
|
+
export declare function manifestPath(projectRoot: string): string;
|
|
46
|
+
export declare function manifestExists(projectRoot: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Read the manifest, falling back to a synthesized one.
|
|
49
|
+
*
|
|
50
|
+
* A malformed manifest throws — unlike a missing one. Silently ignoring a file
|
|
51
|
+
* the developer wrote, and building something else instead, is the worst
|
|
52
|
+
* available behaviour.
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadManifest(projectRoot: string): LoadedManifest;
|
|
55
|
+
/** Write a manifest, with a trailing newline so it plays well with other tools. */
|
|
56
|
+
export declare function writeManifest(projectRoot: string, manifest: RebaseProjectManifest): string;
|
|
57
|
+
/** Find the single backend app, if this repository declares one. */
|
|
58
|
+
export declare function findBackendApp(manifest: RebaseProjectManifest): {
|
|
59
|
+
name: string;
|
|
60
|
+
app: RebaseBackendAppConfig;
|
|
61
|
+
} | undefined;
|
|
62
|
+
/** Apps that produce build output, in the order they should be built. */
|
|
63
|
+
export declare function buildableApps(manifest: RebaseProjectManifest): {
|
|
64
|
+
name: string;
|
|
65
|
+
app: RebaseAppConfig;
|
|
66
|
+
}[];
|
|
67
|
+
/**
|
|
68
|
+
* Decide whether a project can run on the managed runtime, and say why not.
|
|
69
|
+
*
|
|
70
|
+
* "Not eligible" is never a dead end — it selects the custom-runtime path, which
|
|
71
|
+
* still deploys. The reasons exist so the answer is actionable rather than a
|
|
72
|
+
* verdict.
|
|
73
|
+
*/
|
|
74
|
+
export declare function assessManagedCompatibility(manifest: RebaseProjectManifest): ManagedCompatibility;
|
|
75
|
+
/** Resolve a backend app's directories against the conventions it omits. */
|
|
76
|
+
export declare function resolveBackendPaths(app: RebaseBackendAppConfig): {
|
|
77
|
+
config: string;
|
|
78
|
+
functions: string;
|
|
79
|
+
crons: string;
|
|
80
|
+
schema: string;
|
|
81
|
+
usersCollection: string;
|
|
82
|
+
mode: "cms" | "baas";
|
|
83
|
+
};
|
|
@@ -19,14 +19,38 @@ export interface PMCommands {
|
|
|
19
19
|
/** The workspace dependency protocol: `"workspace:*"` for pnpm, `"*"` for npm. */
|
|
20
20
|
workspaceProtocol: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Decide availability from a `spawnSync` outcome.
|
|
24
|
+
*
|
|
25
|
+
* Split out from the spawn itself so the decision is testable without starting
|
|
26
|
+
* a process — which is what made the old test load-sensitive and occasionally
|
|
27
|
+
* red for reasons that had nothing to do with the code under test.
|
|
28
|
+
*
|
|
29
|
+
* The three outcomes are distinguishable, and the old code conflated two of
|
|
30
|
+
* them by asking only `status === 0`:
|
|
31
|
+
*
|
|
32
|
+
* not installed status null, signal null, error.code ENOENT
|
|
33
|
+
* timed out status null, signal SIGTERM, error.code ETIMEDOUT
|
|
34
|
+
* broken install status non-zero, no error
|
|
35
|
+
*/
|
|
36
|
+
export declare function pnpmAvailabilityFromProbe(res: {
|
|
37
|
+
status: number | null;
|
|
38
|
+
signal?: NodeJS.Signals | null;
|
|
39
|
+
error?: {
|
|
40
|
+
code?: string;
|
|
41
|
+
} | Error;
|
|
42
|
+
}): boolean;
|
|
22
43
|
/**
|
|
23
44
|
* Whether pnpm is runnable on this machine.
|
|
24
45
|
*
|
|
25
46
|
* Used to decide whether a fresh project can be scaffolded with pnpm. Kept
|
|
26
|
-
* cheap and non-interactive (
|
|
27
|
-
* hangs detection if a corepack shim misbehaves
|
|
47
|
+
* cheap and non-interactive (bounded timeout, output discarded) so it never
|
|
48
|
+
* hangs detection if a corepack shim misbehaves, and memoised so that repeated
|
|
49
|
+
* detection in one CLI run costs one process rather than one per call.
|
|
28
50
|
*/
|
|
29
51
|
export declare function isPnpmAvailable(): boolean;
|
|
52
|
+
/** Forget the memoised probe. For tests; nothing in a CLI run needs it. */
|
|
53
|
+
export declare function resetPnpmAvailabilityCache(): void;
|
|
30
54
|
/**
|
|
31
55
|
* Detect the package manager for a Rebase project.
|
|
32
56
|
*
|
package/dist/utils/project.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
/** The authored project manifest. Its presence alone marks a project root. */
|
|
2
|
+
export declare const MANIFEST_FILENAME = "rebase.json";
|
|
1
3
|
/**
|
|
2
4
|
* Walk up from `startDir` to find the Rebase project root.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* A directory is the root when it holds a `rebase.json`, or when it holds a
|
|
7
|
+
* `package.json` that either lists `backend` as a workspace or sits beside both
|
|
8
|
+
* `backend/` and `config/`.
|
|
9
|
+
*
|
|
10
|
+
* `rebase.json` is checked first and needs no `package.json` beside it, because
|
|
11
|
+
* the conventions below all describe a repository that *contains the backend*.
|
|
12
|
+
* A repository holding only a frontend — the normal shape once a project's apps
|
|
13
|
+
* live in separate repositories — matches none of them, so without this the
|
|
14
|
+
* tooling could not run there at all.
|
|
7
15
|
*/
|
|
8
16
|
export declare function findProjectRoot(startDir?: string): string | null;
|
|
9
17
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1-canary.0a881d4",
|
|
4
4
|
"description": "Developer tools for Rebase projects",
|
|
5
5
|
"main": "./dist/index.es.js",
|
|
6
6
|
"module": "./dist/index.es.js",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"execa": "^9.6.1",
|
|
32
32
|
"inquirer": "14.0.2",
|
|
33
33
|
"jiti": "^2.7.0",
|
|
34
|
-
"@rebasepro/
|
|
35
|
-
"@rebasepro/
|
|
36
|
-
"@rebasepro/
|
|
37
|
-
"@rebasepro/server-postgres": "0.10.
|
|
38
|
-
"@rebasepro/
|
|
39
|
-
"@rebasepro/
|
|
34
|
+
"@rebasepro/client": "0.10.1-canary.0a881d4",
|
|
35
|
+
"@rebasepro/agent-skills": "0.10.1-canary.0a881d4",
|
|
36
|
+
"@rebasepro/codegen": "0.10.1-canary.0a881d4",
|
|
37
|
+
"@rebasepro/server-postgres": "0.10.1-canary.0a881d4",
|
|
38
|
+
"@rebasepro/server": "0.10.1-canary.0a881d4",
|
|
39
|
+
"@rebasepro/types": "0.10.1-canary.0a881d4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^25.9.3",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"files": [
|
|
50
50
|
"bin/",
|
|
51
|
+
"runtime/",
|
|
51
52
|
"dist/",
|
|
52
53
|
"templates/"
|
|
53
54
|
],
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The development entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Run under `tsx` by `rebase dev` for projects that have no hand-written
|
|
5
|
+
* `backend/src/index.ts`. It boots the same runtime a deployment does, over the
|
|
6
|
+
* project's TypeScript **source** rather than a compiled bundle — so hot reload
|
|
7
|
+
* still works, and development still predicts production because both go through
|
|
8
|
+
* one boot path.
|
|
9
|
+
*
|
|
10
|
+
* Paths arrive in the environment rather than as arguments because `tsx watch`
|
|
11
|
+
* owns the argument list.
|
|
12
|
+
*/
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import dotenv from "dotenv";
|
|
16
|
+
|
|
17
|
+
const projectRoot = process.env.REBASE_DEV_PROJECT_ROOT || process.cwd();
|
|
18
|
+
|
|
19
|
+
const envFile = process.env.DOTENV_CONFIG_PATH;
|
|
20
|
+
if (envFile && fs.existsSync(envFile)) {
|
|
21
|
+
dotenv.config({ path: envFile });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { createSourceBundle, runFromBundle } = await import("@rebasepro/server");
|
|
25
|
+
|
|
26
|
+
const optional = (value) => (value && value.length > 0 ? value : undefined);
|
|
27
|
+
|
|
28
|
+
const bundle = createSourceBundle({
|
|
29
|
+
projectRoot,
|
|
30
|
+
config: optional(process.env.REBASE_DEV_CONFIG),
|
|
31
|
+
functions: optional(process.env.REBASE_DEV_FUNCTIONS),
|
|
32
|
+
crons: optional(process.env.REBASE_DEV_CRONS),
|
|
33
|
+
schema: optional(process.env.REBASE_DEV_SCHEMA),
|
|
34
|
+
mode: process.env.REBASE_DEV_MODE === "baas" ? "baas" : "cms",
|
|
35
|
+
app: optional(process.env.REBASE_DEV_APP)
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// The dev port file and the graceful-shutdown handlers both key off the project
|
|
39
|
+
// root, so make sure the process agrees with the CLI about where that is.
|
|
40
|
+
process.chdir(projectRoot);
|
|
41
|
+
void path;
|
|
42
|
+
|
|
43
|
+
await runFromBundle({ bundle });
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@rebasepro/server";
|
|
16
16
|
import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
|
|
17
17
|
import { env } from "./env.js";
|
|
18
|
+
import { storageAuthorize } from "./storage.js";
|
|
18
19
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
21
|
const __dirname = path.dirname(__filename);
|
|
@@ -122,6 +123,12 @@ pass: env.SMTP_PASS! }
|
|
|
122
123
|
}
|
|
123
124
|
: undefined
|
|
124
125
|
},
|
|
126
|
+
// File storage is opt-in. With no bucket configured, storage is OFF in
|
|
127
|
+
// production — the upload routes answer 501 STORAGE_NOT_CONFIGURED —
|
|
128
|
+
// rather than writing to the container filesystem, which is erased on
|
|
129
|
+
// every restart and redeploy. Uploads that fail loudly are recoverable;
|
|
130
|
+
// uploads that succeed into a disk about to be wiped are not.
|
|
131
|
+
// Local disk stays the default in development, where it is what you want.
|
|
125
132
|
storage: env.STORAGE_TYPE === "s3"
|
|
126
133
|
? {
|
|
127
134
|
type: "s3",
|
|
@@ -132,10 +139,27 @@ pass: env.SMTP_PASS! }
|
|
|
132
139
|
endpoint: env.S3_ENDPOINT,
|
|
133
140
|
forcePathStyle: env.S3_FORCE_PATH_STYLE
|
|
134
141
|
}
|
|
135
|
-
:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
: env.STORAGE_TYPE === "gcs"
|
|
143
|
+
? {
|
|
144
|
+
type: "gcs",
|
|
145
|
+
bucket: env.GCS_BUCKET!,
|
|
146
|
+
projectId: env.GCS_PROJECT_ID,
|
|
147
|
+
keyFilename: env.GCS_KEY_FILENAME
|
|
148
|
+
}
|
|
149
|
+
// Set FORCE_LOCAL_STORAGE=true only if this deployment really
|
|
150
|
+
// does have a durable volume mounted at STORAGE_PATH.
|
|
151
|
+
: isProduction && !env.FORCE_LOCAL_STORAGE
|
|
152
|
+
? undefined
|
|
153
|
+
: {
|
|
154
|
+
type: "local",
|
|
155
|
+
basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
|
|
156
|
+
},
|
|
157
|
+
// Storage is not under row-level security, so this hook IS its access
|
|
158
|
+
// model — the server refuses to boot in production without one, because
|
|
159
|
+
// "signed in" would otherwise be the only thing between a caller and every
|
|
160
|
+
// file in the bucket. See storage.ts; the default scopes each caller to
|
|
161
|
+
// `users/<uid>/` and is meant to be replaced with your own rule.
|
|
162
|
+
storageAuthorize,
|
|
139
163
|
history: true,
|
|
140
164
|
enableSwagger: true
|
|
141
165
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Who may do what to files in storage.
|
|
5
|
+
*
|
|
6
|
+
* Storage is **not** under row-level security. Tables are: a request runs as
|
|
7
|
+
* `rebase_user` and Postgres decides row by row. Object storage has no equivalent,
|
|
8
|
+
* so keys share one flat namespace and this hook is the whole authorization model.
|
|
9
|
+
* Without it the server refuses to boot in production, because "authenticated"
|
|
10
|
+
* would be the only thing between a signed-in caller and every file in the bucket —
|
|
11
|
+
* they can `GET /storage/list?prefix=` to enumerate every key, then read, overwrite
|
|
12
|
+
* or delete any of them.
|
|
13
|
+
*
|
|
14
|
+
* It lives in `backend/src/` rather than a config package because BaaS mode has no
|
|
15
|
+
* config package: collections come from the live database, so there is nothing to
|
|
16
|
+
* declare and nowhere else to put this.
|
|
17
|
+
*
|
|
18
|
+
* ## The default this starter ships
|
|
19
|
+
*
|
|
20
|
+
* A deliberately conservative one, because a BaaS project's storage semantics are
|
|
21
|
+
* whatever *your* application says they are and this file cannot know:
|
|
22
|
+
*
|
|
23
|
+
* | operation | who |
|
|
24
|
+
* | --- | --- |
|
|
25
|
+
* | `read` of `public/…` | anyone, signed in or not |
|
|
26
|
+
* | everything else | a signed-in caller under their own `users/<uid>/` prefix |
|
|
27
|
+
*
|
|
28
|
+
* Gating `list` on the prefix is the important half. Enumeration is what turns "keys
|
|
29
|
+
* are hard to guess" into "keys are known", and it is step one of the attack the
|
|
30
|
+
* boot guard exists to prevent.
|
|
31
|
+
*
|
|
32
|
+
* ## Replace this with your model
|
|
33
|
+
*
|
|
34
|
+
* Prefix rules are a starting point, not a general answer: they work for writes
|
|
35
|
+
* because a write *puts* the object there, but anything already in the bucket needs
|
|
36
|
+
* the row that owns it consulted. `ctx.data` is handed in for exactly that — trusted,
|
|
37
|
+
* RLS-bypassing reads, because the hook *is* the authorization decision and running
|
|
38
|
+
* it through a reader already narrowed by the caller's permissions would be circular:
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* export const storageAuthorize: StorageAuthorize = async ({ key, user, operation, data }) => {
|
|
42
|
+
* if (operation === "read" && isPublicStoragePath(key)) return true;
|
|
43
|
+
* if (!user) return false;
|
|
44
|
+
* const row = (await data?.collection("attachments").find({
|
|
45
|
+
* where: { storage_key: ["==", key] }, limit: 1
|
|
46
|
+
* }))?.data?.[0];
|
|
47
|
+
* return row?.owner_id === user.uid;
|
|
48
|
+
* };
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* If your bucket really is a public read-only CDN, `storagePublicRead: true` says so
|
|
52
|
+
* instead. If every signed-in caller is genuinely trusted with every file,
|
|
53
|
+
* `storageInsecureAllowAnyAuthenticated: true` says *that* — it is named to be read
|
|
54
|
+
* twice. Both are set on `initializeRebaseBackend` in `index.ts`.
|
|
55
|
+
*/
|
|
56
|
+
export const storageAuthorize: StorageAuthorize = ({ key, user, operation }) => {
|
|
57
|
+
// The `public/` prefix is the framework's convention for world-readable objects.
|
|
58
|
+
if (operation === "read" && isPublicStoragePath(key)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// `user` is null on routes that allow unauthenticated access.
|
|
63
|
+
if (!user) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Each caller gets their own namespace. A listing is scoped the same way, so a
|
|
68
|
+
// prefix outside it — including the empty prefix, which would enumerate the
|
|
69
|
+
// whole bucket — is denied.
|
|
70
|
+
return key.startsWith(`users/${user.uid}/`);
|
|
71
|
+
};
|
|
@@ -80,12 +80,19 @@ VITE_API_URL=http://localhost:3001
|
|
|
80
80
|
# ── Storage ───────────────────────────────────────────────────────────────────
|
|
81
81
|
# Set STORAGE_TYPE to choose a storage backend. Default is "local" (filesystem).
|
|
82
82
|
#
|
|
83
|
-
# Supported values: local, s3
|
|
83
|
+
# Supported values: local, s3, gcs
|
|
84
|
+
#
|
|
85
|
+
# In production, "local" means NO file storage: uploads answer
|
|
86
|
+
# 501 STORAGE_NOT_CONFIGURED instead of landing on a container filesystem that
|
|
87
|
+
# is erased on the next redeploy. Configure a bucket below to switch it on, or
|
|
88
|
+
# set FORCE_LOCAL_STORAGE=true if a durable volume really is mounted at
|
|
89
|
+
# STORAGE_PATH (the generated docker-compose.yml does exactly that).
|
|
84
90
|
#
|
|
85
91
|
# STORAGE_TYPE=local
|
|
86
92
|
|
|
87
93
|
# --- Local filesystem (default — great for dev and single-server deployments) --
|
|
88
94
|
# STORAGE_PATH=./uploads
|
|
95
|
+
# FORCE_LOCAL_STORAGE=true # Only with a durable volume mounted at STORAGE_PATH
|
|
89
96
|
|
|
90
97
|
# --- S3-compatible (AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage, Backblaze B2) ---
|
|
91
98
|
# STORAGE_TYPE=s3
|
|
@@ -103,9 +110,15 @@ VITE_API_URL=http://localhost:3001
|
|
|
103
110
|
# S3_ENDPOINT=https://storage.googleapis.com
|
|
104
111
|
# S3_ACCESS_KEY_ID=GOOG... # HMAC access key
|
|
105
112
|
# S3_SECRET_ACCESS_KEY=... # HMAC secret
|
|
113
|
+
|
|
114
|
+
# --- Native GCS (Google Cloud Storage, Firebase Storage) ---
|
|
115
|
+
# STORAGE_TYPE=gcs
|
|
116
|
+
# GCS_BUCKET=my-gcs-bucket
|
|
117
|
+
# GCS_PROJECT_ID= # Optional — auto-detected from credentials
|
|
118
|
+
# GCS_KEY_FILENAME= # Omit on GCP: Workload Identity/ADC supplies credentials
|
|
106
119
|
#
|
|
107
|
-
# For
|
|
108
|
-
#
|
|
120
|
+
# For other providers (Azure Blob, etc.), implement the StorageController
|
|
121
|
+
# interface and pass it directly in your backend config.
|
|
109
122
|
|
|
110
123
|
# ── Backups (optional) ────────────────────────────────────────────────────────
|
|
111
124
|
# Manual backups: `rebase db backup --out ./backups` (or an s3://… URL).
|
|
@@ -4,57 +4,53 @@ A [Rebase](https://rebase.pro) project with a PostgreSQL backend.
|
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`rebase init` already generated a `.env` for you with a real `JWT_SECRET`, a
|
|
8
|
+
database password, and a free local database port. **Don't run
|
|
9
|
+
`cp .env.example .env`** — that would overwrite those generated values.
|
|
10
|
+
`.env.example` is a reference for the variables you can set, not a starting
|
|
11
|
+
point for this project.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
cp .env.example .env
|
|
11
|
-
# Edit .env — set JWT_SECRET and DATABASE_URL (see comments for generators)
|
|
12
|
-
|
|
13
|
-
docker compose up -d
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
That's it. Your app is running:
|
|
17
|
-
- **Frontend**: http://localhost (port 80)
|
|
18
|
-
- **Backend API**: http://localhost:3001
|
|
19
|
-
- **PostgreSQL**: localhost:5432
|
|
20
|
-
|
|
21
|
-
### Option 2: Local Development
|
|
22
|
-
|
|
23
|
-
#### Prerequisites
|
|
13
|
+
### Prerequisites
|
|
24
14
|
|
|
25
15
|
- [Node.js](https://nodejs.org) >= 18
|
|
26
16
|
- [pnpm](https://pnpm.io) or [npm](https://www.npmjs.com) (v7+)
|
|
27
|
-
-
|
|
17
|
+
- [Docker](https://www.docker.com) (to run the included PostgreSQL container),
|
|
18
|
+
or your own PostgreSQL database
|
|
28
19
|
|
|
29
|
-
|
|
20
|
+
### Run it
|
|
30
21
|
|
|
31
|
-
1. Install dependencies:
|
|
22
|
+
1. Install dependencies (skip if `init` already did this):
|
|
32
23
|
|
|
33
24
|
```bash
|
|
34
25
|
pnpm install # or: npm install
|
|
35
26
|
```
|
|
36
27
|
|
|
37
|
-
2.
|
|
28
|
+
2. Start the PostgreSQL database container:
|
|
38
29
|
|
|
39
30
|
```bash
|
|
40
|
-
|
|
41
|
-
# Edit .env — set DATABASE_URL, JWT_SECRET
|
|
31
|
+
docker compose up -d db
|
|
42
32
|
```
|
|
43
33
|
|
|
44
|
-
3.
|
|
34
|
+
3. Create the database tables from your collections:
|
|
45
35
|
|
|
46
36
|
```bash
|
|
47
|
-
pnpm run
|
|
48
|
-
pnpm run db:push # or: npm run db:push
|
|
37
|
+
pnpm run db:push # or: npm run db:push
|
|
49
38
|
```
|
|
50
39
|
|
|
51
|
-
4. Start the dev
|
|
40
|
+
4. Start the dev servers:
|
|
52
41
|
|
|
53
42
|
```bash
|
|
54
43
|
pnpm dev # or: npm run dev
|
|
55
44
|
```
|
|
56
45
|
|
|
57
|
-
|
|
46
|
+
Open **http://localhost:5173** — the admin panel. The first account you
|
|
47
|
+
register becomes the admin. The backend API (Hono + PostgreSQL) runs on
|
|
48
|
+
port 3001; the frontend (Vite + React) on port 5173.
|
|
49
|
+
|
|
50
|
+
> The `db:push` step is what creates the tables for the example `posts`,
|
|
51
|
+
> `authors`, and `tags` collections. Skip it and the admin panel still opens,
|
|
52
|
+
> but those collections will be empty and their API calls will fail until the
|
|
53
|
+
> tables exist.
|
|
58
54
|
|
|
59
55
|
## Project Structure
|
|
60
56
|
|
|
@@ -102,12 +98,21 @@ All configuration is managed through a single `.env` file in the project root. B
|
|
|
102
98
|
- **Frontend**: Vite reads `VITE_*` variables via `envDir` pointing to the project root
|
|
103
99
|
- **Scripts**: load via `dotenv` from the project root
|
|
104
100
|
|
|
105
|
-
|
|
101
|
+
`init` already generated your `.env`. See the comments in `.env.example` for details on each variable, and edit `.env` directly to change any of them.
|
|
106
102
|
|
|
107
103
|
## Production Deployment
|
|
108
104
|
|
|
105
|
+
The full stack — PostgreSQL, backend, and frontend — runs from
|
|
106
|
+
`docker compose`. The backend image builds and boots, but it does **not**
|
|
107
|
+
create your collection tables on its own, so push the schema once the
|
|
108
|
+
database is up before (or right after) starting the rest of the stack.
|
|
109
|
+
|
|
109
110
|
```bash
|
|
110
|
-
#
|
|
111
|
+
# 1. Start the database and create the tables from your collections
|
|
112
|
+
docker compose up -d db
|
|
113
|
+
pnpm run db:push # or: npm run db:push
|
|
114
|
+
|
|
115
|
+
# 2. Build and start the backend + frontend
|
|
111
116
|
docker compose up -d --build
|
|
112
117
|
|
|
113
118
|
# View logs
|
|
@@ -120,6 +125,11 @@ docker compose down
|
|
|
120
125
|
docker compose down -v
|
|
121
126
|
```
|
|
122
127
|
|
|
128
|
+
> `docker compose` reads the generated `.env` as-is. Set production values
|
|
129
|
+
> (a strong `DATABASE_PASSWORD`, `JWT_SECRET`, storage credentials, …) by
|
|
130
|
+
> editing `.env` directly — see `.env.example` for the full list. Do not
|
|
131
|
+
> `cp .env.example .env`; that discards the values `init` generated.
|
|
132
|
+
|
|
123
133
|
## Documentation
|
|
124
134
|
|
|
125
135
|
- [Rebase Docs](https://rebase.pro/docs)
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "@rebasepro/server";
|
|
17
17
|
import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
|
|
18
18
|
import { enums, relations, tables } from "./schema.generated.js";
|
|
19
|
+
import { storageAuthorize } from "../../config/storage.js";
|
|
19
20
|
import { env } from "./env.js";
|
|
20
21
|
import usersCollection from "../../config/collections/users.js";
|
|
21
22
|
|
|
@@ -123,6 +124,12 @@ pass: env.SMTP_PASS! }
|
|
|
123
124
|
}
|
|
124
125
|
: undefined
|
|
125
126
|
},
|
|
127
|
+
// File storage is opt-in. With no bucket configured, storage is OFF in
|
|
128
|
+
// production — the upload routes answer 501 STORAGE_NOT_CONFIGURED —
|
|
129
|
+
// rather than writing to the container filesystem, which is erased on
|
|
130
|
+
// every restart and redeploy. Uploads that fail loudly are recoverable;
|
|
131
|
+
// uploads that succeed into a disk about to be wiped are not.
|
|
132
|
+
// Local disk stays the default in development, where it is what you want.
|
|
126
133
|
storage: env.STORAGE_TYPE === "s3"
|
|
127
134
|
? {
|
|
128
135
|
type: "s3",
|
|
@@ -133,10 +140,27 @@ pass: env.SMTP_PASS! }
|
|
|
133
140
|
endpoint: env.S3_ENDPOINT,
|
|
134
141
|
forcePathStyle: env.S3_FORCE_PATH_STYLE
|
|
135
142
|
}
|
|
136
|
-
:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
: env.STORAGE_TYPE === "gcs"
|
|
144
|
+
? {
|
|
145
|
+
type: "gcs",
|
|
146
|
+
bucket: env.GCS_BUCKET!,
|
|
147
|
+
projectId: env.GCS_PROJECT_ID,
|
|
148
|
+
keyFilename: env.GCS_KEY_FILENAME
|
|
149
|
+
}
|
|
150
|
+
// Set FORCE_LOCAL_STORAGE=true only if this deployment really
|
|
151
|
+
// does have a durable volume mounted at STORAGE_PATH.
|
|
152
|
+
: isProduction && !env.FORCE_LOCAL_STORAGE
|
|
153
|
+
? undefined
|
|
154
|
+
: {
|
|
155
|
+
type: "local",
|
|
156
|
+
basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
|
|
157
|
+
},
|
|
158
|
+
// Storage is not under row-level security, so this hook IS its access
|
|
159
|
+
// model — the server refuses to boot in production without one, because
|
|
160
|
+
// "signed in" would otherwise be the only thing between a visitor and
|
|
161
|
+
// every file in the bucket. See config/storage.ts, which also shows the
|
|
162
|
+
// multi-tenant (per-owner) shape and the two escape hatches.
|
|
163
|
+
storageAuthorize,
|
|
140
164
|
history: true
|
|
141
165
|
});
|
|
142
166
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/// <reference types="@rebasepro/admin-types" />
|
|
2
|
+
|
|
3
|
+
// One line, once per project. `@rebasepro/admin-types` declares the `admin` block on
|
|
4
|
+
// `CollectionConfig` and on every property type by declaration merging, and an
|
|
5
|
+
// augmentation applies to the whole *program* — so this reference is what makes
|
|
6
|
+
// `admin: { … }` legal in every collection file here.
|
|
7
|
+
//
|
|
8
|
+
// A BaaS project has no admin panel and no reason for this file. Without it, `admin` on
|
|
9
|
+
// a collection or a property is a type error, which is the guarantee.
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PostgresCollectionConfig } from "@rebasepro/types";
|
|
2
2
|
|
|
3
|
-
const authorsCollection:
|
|
3
|
+
const authorsCollection: PostgresCollectionConfig = {
|
|
4
4
|
name: "Authors",
|
|
5
5
|
singularName: "Author",
|
|
6
6
|
slug: "authors",
|
|
7
7
|
table: "authors",
|
|
8
|
-
icon: "Person",
|
|
9
8
|
properties: {
|
|
10
9
|
id: {
|
|
11
10
|
name: "ID",
|
|
@@ -34,12 +33,15 @@ const authorsCollection: CollectionConfig = {
|
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
35
|
},
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
admin: {
|
|
37
|
+
icon: "Person",
|
|
38
|
+
propertiesOrder: [
|
|
39
|
+
"id",
|
|
40
|
+
"name",
|
|
41
|
+
"email",
|
|
42
|
+
"picture"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
export default authorsCollection;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PostgresCollectionConfig } from "@rebasepro/types";
|
|
2
2
|
import authorsCollection from "./authors.js";
|
|
3
3
|
import tagsCollection from "./tags.js";
|
|
4
4
|
|
|
5
|
-
const postsCollection:
|
|
5
|
+
const postsCollection: PostgresCollectionConfig = {
|
|
6
6
|
name: "Posts",
|
|
7
7
|
singularName: "Post",
|
|
8
8
|
slug: "posts",
|
|
9
9
|
table: "posts",
|
|
10
|
-
icon: "Article",
|
|
11
10
|
properties: {
|
|
12
11
|
id: {
|
|
13
12
|
name: "ID",
|
|
@@ -24,7 +23,7 @@ const postsCollection: CollectionConfig = {
|
|
|
24
23
|
content: {
|
|
25
24
|
name: "Content",
|
|
26
25
|
type: "string",
|
|
27
|
-
|
|
26
|
+
admin: {
|
|
28
27
|
markdown: true
|
|
29
28
|
}
|
|
30
29
|
},
|
|
@@ -65,6 +64,9 @@ const postsCollection: CollectionConfig = {
|
|
|
65
64
|
cardinality: "many",
|
|
66
65
|
direction: "owning"
|
|
67
66
|
}
|
|
67
|
+
},
|
|
68
|
+
admin: {
|
|
69
|
+
icon: "Article"
|
|
68
70
|
}
|
|
69
71
|
};
|
|
70
72
|
|