@@ -126,8 +143,15 @@ export const DeployButton = ({ onVercelDeploy, onNetlifyDeploy }: DeployButtonPr
- Deploy to Cloudflare (Coming Soon)
+ {!cloudflareConn.token ? 'No Cloudflare Token' : 'Deploy to Cloudflare'}
+ {cloudflareConn.token && cloudflareConn.accountId && }
diff --git a/app/lib/runtime/action-runner.ts b/app/lib/runtime/action-runner.ts
index 20db0816..a7234c49 100644
--- a/app/lib/runtime/action-runner.ts
+++ b/app/lib/runtime/action-runner.ts
@@ -522,7 +522,7 @@ export class ActionRunner {
details?: {
url?: string;
error?: string;
- source?: 'netlify' | 'vercel' | 'github';
+ source?: 'netlify' | 'vercel' | 'cloudflare' | 'github';
},
): void {
if (!this.onDeployAlert) {
diff --git a/app/lib/stores/cloudflare.ts b/app/lib/stores/cloudflare.ts
new file mode 100644
index 00000000..04b22c1b
--- /dev/null
+++ b/app/lib/stores/cloudflare.ts
@@ -0,0 +1,43 @@
+import { atom } from 'nanostores';
+import type { CloudflareConnection, CloudflareProject } from '~/types/cloudflare';
+import { logStore } from './logs';
+import { toast } from 'react-toastify';
+
+const storedConnection = typeof window !== 'undefined' ? localStorage.getItem('cloudflare_connection') : null;
+
+const envToken = import.meta.env.VITE_CLOUDFLARE_API_TOKEN;
+const envAccountId = import.meta.env.VITE_CLOUDFLARE_ACCOUNT_ID;
+
+const initialConnection: CloudflareConnection = storedConnection
+ ? JSON.parse(storedConnection)
+ : { accountId: envAccountId || '', token: envToken || '', projects: undefined };
+
+export const cloudflareConnection = atom
(initialConnection);
+export const isConnecting = atom(false);
+
+export const updateCloudflareConnection = (updates: Partial) => {
+ const current = cloudflareConnection.get();
+ const newState = { ...current, ...updates };
+ cloudflareConnection.set(newState);
+
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('cloudflare_connection', JSON.stringify(newState));
+ }
+};
+
+export async function fetchCloudflareProjects(token: string, accountId: string) {
+ try {
+ const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to fetch projects: ${response.status}`);
+ }
+ const data = (await response.json()) as any;
+ updateCloudflareConnection({ projects: data.result as CloudflareProject[] });
+ } catch (error) {
+ console.error('Cloudflare API Error:', error);
+ logStore.logError('Failed to fetch Cloudflare projects', { error });
+ toast.error('Failed to fetch Cloudflare projects');
+ }
+}
diff --git a/app/routes/api.cloudflare-deploy.ts b/app/routes/api.cloudflare-deploy.ts
new file mode 100644
index 00000000..f9facd11
--- /dev/null
+++ b/app/routes/api.cloudflare-deploy.ts
@@ -0,0 +1,70 @@
+import { type ActionFunctionArgs, json } from '@remix-run/cloudflare';
+import JSZip from 'jszip';
+
+interface DeployRequestBody {
+ accountId: string;
+ projectName?: string;
+ files: Record;
+ chatId: string;
+ token: string;
+}
+
+export async function action({ request }: ActionFunctionArgs) {
+ try {
+ const { accountId, projectName, files, token, chatId } = (await request.json()) as DeployRequestBody;
+
+ if (!accountId || !token) {
+ return json({ error: 'Missing Cloudflare credentials' }, { status: 401 });
+ }
+
+ let targetProject = projectName;
+
+ if (!targetProject) {
+ const name = `bolt-diy-${chatId}-${Date.now()}`;
+ const createRes = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name }),
+ });
+ if (!createRes.ok) {
+ const txt = await createRes.text();
+ return json({ error: `Failed to create project: ${txt}` }, { status: 400 });
+ }
+ targetProject = name;
+ }
+
+ const zip = new JSZip();
+ for (const [filePath, content] of Object.entries(files)) {
+ const normalized = filePath.startsWith('/') ? filePath.substring(1) : filePath;
+ zip.file(normalized, content);
+ }
+ const zipData = await zip.generateAsync({ type: 'nodebuffer' });
+
+ const form = new FormData();
+ form.append('metadata', JSON.stringify({}));
+ form.append('file', new Blob([zipData]), 'deploy.zip');
+
+ const deployRes = await fetch(
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${targetProject}/deployments`,
+ {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` },
+ body: form as any,
+ },
+ );
+
+ const deployData = await deployRes.json();
+ if (!deployRes.ok) {
+ return json({ error: deployData.errors?.[0]?.message || 'Failed to deploy' }, { status: 400 });
+ }
+
+ return json({
+ success: true,
+ deploy: { id: deployData.result.id, url: deployData.result.url },
+ project: { name: targetProject, id: deployData.result.project_id },
+ });
+ } catch (error) {
+ console.error('Cloudflare deploy error:', error);
+ return json({ error: 'Deployment failed' }, { status: 500 });
+ }
+}
diff --git a/app/types/actions.ts b/app/types/actions.ts
index 0e1411d8..b997bfdc 100644
--- a/app/types/actions.ts
+++ b/app/types/actions.ts
@@ -59,7 +59,7 @@ export interface DeployAlert {
stage?: 'building' | 'deploying' | 'complete';
buildStatus?: 'pending' | 'running' | 'complete' | 'failed';
deployStatus?: 'pending' | 'running' | 'complete' | 'failed';
- source?: 'vercel' | 'netlify' | 'github';
+ source?: 'vercel' | 'netlify' | 'cloudflare' | 'github';
}
export interface FileHistory {
diff --git a/app/types/cloudflare.ts b/app/types/cloudflare.ts
new file mode 100644
index 00000000..2f42c41d
--- /dev/null
+++ b/app/types/cloudflare.ts
@@ -0,0 +1,11 @@
+export interface CloudflareProject {
+ id: string;
+ name: string;
+ subdomain?: string;
+}
+
+export interface CloudflareConnection {
+ accountId: string;
+ token: string;
+ projects?: CloudflareProject[];
+}
diff --git a/docs/docs/index.md b/docs/docs/index.md
index e5f9908a..ce637280 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -40,6 +40,7 @@ Also [this pinned post in our community](https://thinktank.ottomator.ai/t/videos
- **Revert code to earlier versions** for easier debugging and quicker changes.
- **Download projects as ZIP** for easy portability.
- **Integration-ready Docker support** for a hassle-free setup.
+- **One-click deployment** to **Netlify**, **Vercel**, or **Cloudflare Pages**.
---