diff --git a/apps/docs/content/docs/compute/deploy-button.mdx b/apps/docs/content/docs/compute/deploy-button.mdx new file mode 100644 index 0000000000..b007d6a6d6 --- /dev/null +++ b/apps/docs/content/docs/compute/deploy-button.mdx @@ -0,0 +1,160 @@ +--- +title: Deploy Button +description: Add a Deploy with Prisma button to your repository — one click copies your project into the user's GitHub account, provisions a Prisma Postgres database, and deploys it on Prisma Compute. +url: /compute/deploy-button +metaTitle: Deploy Button | Prisma Compute +metaDescription: Let anyone deploy your TypeScript project in one click. The Deploy with Prisma button clones your public GitHub repository, provisions Prisma Postgres, and deploys on Prisma Compute. +--- + +The Deploy Button lets anyone deploy your project in one click. Clicking it opens the Prisma Console, where the user picks a GitHub account and a region — Prisma then copies your repository's code into a new repository they own, creates a project with a [Prisma Postgres](/postgres) database, and starts the first deployment on [Prisma Compute](/compute). + +Add it to your README, a blog post, or your own site: + + + Deploy with Prisma + + +The button supports public GitHub repositories; support for private repositories is planned. + +## Make your repository deployable + +Any public GitHub repository works with the Deploy Button when it meets this minimal contract: + +- **Public GitHub repository.** The button deploys the repository's default branch. +- **A `prisma.compute.json` file at the repository root.** This is the same file Prisma Compute reads for any connected repository — it names the app and sets its HTTP port. It must describe a single app, and it must not set a `region`: the person deploying picks the region, which is used for both the database and the app. See [configuration](/compute/configuration). +- **A TypeScript project with a `package.json` and a lockfile** (`bun.lock`, `package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`) at the root. +- **A runnable app.** The project must build and start under the normal Compute build: it listens on the `httpPort` declared in `prisma.compute.json`, and anything it needs beyond `DATABASE_URL` is declared as [required environment variables](#environment-variables) on the button link. + +The repository is validated before it is copied, so a few shapes are rejected: + +- More than 100 files, a file over 5 MB, or more than 25 MB in total. +- GitHub Actions workflows (anything under `.github/workflows/`). +- Symlinks or Git submodules. + +There is no registration, API key, or approval process. A minimal working example you can copy is [`prisma/compute-deploy-example`](https://github.com/prisma/compute-deploy-example), whose README carries its own Deploy Button. + +A minimal `prisma.compute.json`: + +```json +{ + "app": { + "name": "my-app", + "httpPort": 3000 + } +} +``` + +## How it works + +1. **Click the button.** The user lands on the clone page in the Prisma Console, which shows what will be deployed: your repository's name — and, once a GitHub account is connected, the resolved commit — linked back to the source on GitHub. If they're signed out, they sign in first and return to the same page automatically. +2. **Choose where it goes.** They pick a workspace (when they have more than one), a GitHub account or organization, a name and visibility for their new repository (private by default), and a deployment region. If the button link declares [required environment variables](#environment-variables), the form prompts for their values here. If the Prisma GitHub App isn't installed yet, the flow walks them through installing it. +3. **Authorize once.** GitHub asks the user to authorize the repository creation. This authorization is used for that single deployment and is not stored. +4. **Prisma sets everything up.** Your code is copied — pinned to the default-branch commit resolved when they click Deploy — into a new repository under the user's account. A project with a Prisma Postgres database is provisioned, the database connection is configured automatically, and the first deployment starts. +5. **Push to deploy.** The new repository is connected to the project, so every later push deploys through the normal Git workflow. + +The user owns the copy and the project. Prisma adds no credentials or platform files to the repository — database connection settings are managed in the Console, not committed to code. + +## Requirements + +For you, the author: only the [repository contract](#make-your-repository-deployable) above. + +For the user clicking the button: + +- A [Prisma Data Platform account](https://pris.ly/pdp) (free to create during the flow). +- A GitHub account with the Prisma GitHub App installed on the account or organization that will own the new repository — the flow offers the installation step when it's missing. +- Permission to create repositories under the selected account or organization. + +`DATABASE_URL` never needs to be configured: it is provisioned and wired automatically. If your app needs other variables, declare their names on the button link and the Console prompts the user for values during the flow; see [environment variables](#environment-variables) below. + +## URL format + +A Deploy Button is a link to: + +```text +https://console.prisma.io/new/clone?repository-url= +``` + +| Parameter | Required | Description | +| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `repository-url` | Yes | The public GitHub repository to deploy, in the form `https://github.com/owner/repo`. URL-encode it when composing the link. | +| `project-name` | No | Prefills the name of the repository the user creates (they can change it). Defaults to your repository's name. | +| `env` | No | Comma-separated names of environment variables your app requires, for example `env=OPENAI_API_KEY,RESEND_API_KEY`. The Console prompts the user for each value during the flow. Up to 10 names of uppercase letters, numbers, and underscores. Never put values in the URL. | +| `env-example-` | No | A safe, non-secret example value that prefills the input for ``, for example `env-example-RESEND_API_KEY=re_example_123`. Only use placeholder values that are safe to publish. | +| `utm_source` | No | Attribution for where the click came from, for example `github-readme`. This is the value Prisma's own funnel reporting records. | +| `utm_medium` | No | Attribution for the kind of placement, for example `button`. Carried on the link for your own analytics. | +| `utm_campaign` | No | Attribution for a specific campaign or launch. Carried on the link for your own analytics. | + +Example with a project name and attribution: + +```text +https://console.prisma.io/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Facme%2Fshop-api&project-name=shop-api&utm_source=github-readme +``` + +## Environment variables + +If your app needs configuration beyond the database connection, declare the variable names on the button link: + +```text +https://console.prisma.io/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Facme%2Fshop-api&env=OPENAI_API_KEY,RESEND_API_KEY&env-example-RESEND_API_KEY=re_example_123 +``` + +The Console then prompts the user for a value for each declared variable before the deployment starts. The values are stored encrypted in the user's new project and applied to the first deployment, so the app boots fully configured. The user can change them later in the project's [environment variables](/compute/environment-variables) settings. + +Two rules keep this safe: + +- **The URL carries names, never values.** The user types each value (or accepts your example) in the Console form. Values never appear in the link, in browser history, or in the GitHub authorization round-trip. +- **Examples must be safe to publish.** An `env-example-` value appears in your README for anyone to read, so use obvious placeholders like `re_example_123`, never a real key. + +`DATABASE_URL` and `DATABASE_URL_POOLED` are provisioned automatically. If you list them in `env`, the Console shows them as "Added automatically" instead of prompting, which is a useful way to signal that your app uses the database. A deploy link with no `env` parameter works exactly as before. + +## Create your button + +Enter your repository URL and copy the generated snippet: + + + +## Snippets + +A complete Markdown example for a README: + +```md +[![Deploy with Prisma](https://www.prisma.io/docs/img/deploy-button.svg)](https://console.prisma.io/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Facme%2Fshop-api&utm_source=github-readme) +``` + +The same button in HTML: + +```html + + Deploy with Prisma + +``` + +## Troubleshooting + +**"This repository cannot be deployed" with a message about `prisma.compute.json`.** The repository is missing the root configuration file. Add a `prisma.compute.json` at the repository root — see [make your repository deployable](#make-your-repository-deployable). + +**The clone page says the repository must be public.** The button only supports public source repositories today. The user's _copy_ can still be private — that's their choice in the deploy form. + +**"The repository changed before Prisma could copy the template. Choose another repository name and try again."** The chosen name usually collides with a repository the user already has. Picking a different name in the form and deploying again resolves it. + +**"This deploy link's environment variable list is not valid."** A name in the `env` parameter doesn't match the required shape. Names use uppercase letters, numbers, and underscores (like `MY_API_KEY`), up to 10 per link. Regenerate the link with valid names. + +**GitHub authorization was canceled or expired.** The authorization step must be completed within 15 minutes. Returning to the clone page and clicking Deploy again starts a fresh attempt. + +**The GitHub account isn't listed.** The Prisma GitHub App isn't installed on that account or organization. The "Connect GitHub" option in the deploy form starts the installation; after completing it on GitHub, the user continues from the clone page. + +**The repository was created, but the deployment didn't start.** Nothing is deleted on a partial failure. If the Prisma project was created and connected to the repository, pushing any commit to the new repository's default branch triggers a deployment through the normal Git flow. If the failure happened before the project existed, return to the clone page and deploy again with a different repository name. See [deployments](/compute/deployments). + +**The app deployed but shows an error page.** Check the build and runtime logs on the project's deployment page, confirm the database finished provisioning in the Console, and verify the app listens on the port declared in `prisma.compute.json`. The [FAQ](/compute/faq) covers common runtime issues. diff --git a/apps/docs/content/docs/compute/meta.json b/apps/docs/content/docs/compute/meta.json index 9a606f5fb0..7aaae82a92 100644 --- a/apps/docs/content/docs/compute/meta.json +++ b/apps/docs/content/docs/compute/meta.json @@ -15,6 +15,7 @@ "domains", "---Integrations---", "github", + "deploy-button", "---Reference---", "configuration", "cli-reference", diff --git a/apps/docs/public/img/deploy-button.svg b/apps/docs/public/img/deploy-button.svg new file mode 100644 index 0000000000..1582425a94 --- /dev/null +++ b/apps/docs/public/img/deploy-button.svg @@ -0,0 +1,8 @@ + + Deploy with Prisma + + + + + Deploy with Prisma + diff --git a/apps/docs/src/components/deploy-button-generator.tsx b/apps/docs/src/components/deploy-button-generator.tsx new file mode 100644 index 0000000000..e345f5f24c --- /dev/null +++ b/apps/docs/src/components/deploy-button-generator.tsx @@ -0,0 +1,361 @@ +"use client"; +import { useMemo, useState } from "react"; +import { useCopyButton } from "fumadocs-ui/utils/use-copy-button"; +import { buttonVariants } from "@prisma-docs/ui/components/button"; +import { cn } from "@prisma-docs/ui/lib/cn"; +import { withDocsBasePath } from "@/lib/urls"; + +const CONSOLE_CLONE_URL = "https://console.prisma.io/new/clone"; +const BUTTON_IMAGE_URL = "https://www.prisma.io/docs/img/deploy-button.svg"; + +const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/; +const REPO_PATTERN = /^[A-Za-z0-9._-]{1,100}$/; +const PROJECT_NAME_PATTERN = /^[A-Za-z0-9._-]{1,100}$/; +// Mirrors the Console's env-var name rules: uppercase names, 10 per link, +// examples must be single-line and safe to publish. +const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/; +const MAX_ENV_VARS = 10; +const MAX_ENV_NAME_LENGTH = 64; +const MAX_ENV_EXAMPLE_LENGTH = 100; + +interface EnvVarRow { + name: string; + example: string; +} + +function envRowValid(row: EnvVarRow): boolean { + const name = row.name.trim(); + const example = row.example.trim(); + if (name === "") return example === ""; + return ( + name.length <= MAX_ENV_NAME_LENGTH && + ENV_NAME_PATTERN.test(name) && + example.length <= MAX_ENV_EXAMPLE_LENGTH + ); +} + +function parseRepositoryUrl( + raw: string, +): { owner: string; repo: string } | null { + const value = raw.trim(); + if (value.length === 0 || value.length > 300) return null; + let url: URL; + try { + url = new URL(value.includes("://") ? value : `https://${value}`); + } catch { + return null; + } + if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port) + return null; + if (url.username || url.password || url.search || url.hash) return null; + const segments = url.pathname.split("/").filter(Boolean); + if (segments.length !== 2) return null; + const owner = segments[0]; + let repo = segments[1]; + if (repo.endsWith(".git")) repo = repo.slice(0, -4); + if (!OWNER_PATTERN.test(owner) || !REPO_PATTERN.test(repo)) return null; + if (repo === "." || repo === "..") return null; + return { owner, repo }; +} + +function CopyButton({ value, label }: { value: string; label: string }) { + const [checked, onClick] = useCopyButton(() => + navigator.clipboard.writeText(value), + ); + + return ( + + ); +} + +function Snippet({ label, value }: { label: string; value: string }) { + return ( +
+ + {label} + +
+
+          {value}
+        
+ +
+
+ ); +} + +function escapeHtmlAttribute(value: string): string { + return value.replaceAll("&", "&").replaceAll('"', """); +} + +const fieldClassName = + "w-full rounded-lg border bg-fd-background px-3 py-2 text-sm text-fd-foreground outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"; + +export function DeployButtonGenerator() { + const [repositoryUrl, setRepositoryUrl] = useState(""); + const [projectName, setProjectName] = useState(""); + const [utmSource, setUtmSource] = useState(""); + const [utmCampaign, setUtmCampaign] = useState(""); + const [envVars, setEnvVars] = useState([ + { name: "", example: "" }, + ]); + + const parsed = useMemo( + () => parseRepositoryUrl(repositoryUrl), + [repositoryUrl], + ); + const projectNameValid = + projectName.trim() === "" || PROJECT_NAME_PATTERN.test(projectName.trim()); + const envVarsValid = envVars.every(envRowValid); + + const url = useMemo(() => { + if (!parsed || !projectNameValid || !envVarsValid) return null; + const search = new URLSearchParams(); + search.set( + "repository-url", + `https://github.com/${parsed.owner}/${parsed.repo}`, + ); + if (projectName.trim()) search.set("project-name", projectName.trim()); + const names: string[] = []; + const examples: [string, string][] = []; + for (const row of envVars) { + const name = row.name.trim(); + if (name === "" || names.includes(name)) continue; + names.push(name); + const example = row.example.trim(); + if (example !== "") examples.push([name, example]); + } + if (names.length > 0) { + search.set("env", names.join(",")); + for (const [name, example] of examples) { + search.set(`env-example-${name}`, example); + } + } + if (utmSource.trim()) search.set("utm_source", utmSource.trim()); + if (utmCampaign.trim()) search.set("utm_campaign", utmCampaign.trim()); + return `${CONSOLE_CLONE_URL}?${search.toString()}`; + }, [ + parsed, + projectNameValid, + envVarsValid, + projectName, + envVars, + utmSource, + utmCampaign, + ]); + + return ( +
+
+ + + + +
+ + Required environment variables{" "} + + (optional) + + +

+ The Console prompts the user for each value during the flow. The + link carries only the names and any example you set here, never real + values, so examples must be safe to publish.{" "} + DATABASE_URL is configured automatically and doesn't + need to be listed. +

+ {envVars.map((row, index) => ( +
+ + setEnvVars((rows) => + rows.map((r, i) => + i === index ? { ...r, name: event.target.value } : r, + ), + ) + } + /> + + setEnvVars((rows) => + rows.map((r, i) => + i === index ? { ...r, example: event.target.value } : r, + ), + ) + } + /> + +
+ ))} + {!envVarsValid ? ( + + Names use uppercase letters, numbers, and underscores, like + MY_API_KEY. An example needs a name next to it. + + ) : null} + {envVars.length < MAX_ENV_VARS ? ( + + ) : null} +
+
+ + {url ? ( + <> + + + + Deploy with Prisma`} + /> + + ) : ( +

+ Enter your repository's GitHub URL to generate the button. The + repository must be public and contain a{" "} + prisma.compute.json file at its root. +

+ )} +
+ ); +} diff --git a/apps/docs/src/mdx-components.tsx b/apps/docs/src/mdx-components.tsx index 5bc64070fd..52f482639f 100644 --- a/apps/docs/src/mdx-components.tsx +++ b/apps/docs/src/mdx-components.tsx @@ -2,6 +2,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import { Youtube } from "@prisma-docs/ui/components/youtube"; import { APIPage } from "@/components/api-page"; import { ConceptAnimation } from "@/components/concept-animation"; +import { DeployButtonGenerator } from "@/components/deploy-button-generator"; import { AgentPrompt, GetStartedTabs, @@ -121,6 +122,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { APIPage, AgentPrompt, ConceptAnimation, + DeployButtonGenerator, GetStartedTabs, HeroGrid, HeroPitch,