# How to Integrate Bolt.new with Google Docs

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 40 minutes
- Last updated: April 2026

## TL;DR

Integrate Bolt.new with Google Docs by enabling the Google Docs API in Google Cloud Console, implementing OAuth 2.0 (requires deployment for the redirect URI), then using Next.js API routes to create documents, read document content, and insert text. For simpler read-only display without OAuth, embed documents via iframe. The OAuth callback cannot be tested in Bolt's WebContainer preview — deploy to Netlify or Bolt Cloud first.

## Building Google Docs Features into Bolt.new Apps

The Google Docs API gives your Bolt application programmatic access to one of the world's most widely used document platforms. With the API, you can create new documents from templates, read structured document content, perform batch text edits, insert tables and images, and update named ranges — all without users having to download, upload, or copy-paste between systems. This is powerful for automating document generation: contract creation from form data, report generation from database records, proposal drafts populated from CRM data, or meeting notes templates auto-filled from calendar events.

What makes the Google Docs API distinctive is its document model. Unlike a traditional word processor that treats a document as a stream of characters, the Google Docs API represents every document as a structured tree of elements: body, paragraphs, runs, tables, table rows, table cells, images, and named ranges. Reading a document returns a deeply nested JSON object describing every structural element with precise position indices. Editing uses a batch of requests (insert, delete, format) that reference character offsets. This design is powerful but requires some adjustment for developers used to simpler APIs.

For simpler use cases where you only need read-only display of a Google Doc, iframe embedding is far faster to implement than the full API. Any Google Doc with link sharing enabled can be embedded as an iframe in your React components with no OAuth required. The tradeoff is that embedded docs display with Google's formatting — you cannot extract or style the text content. For use cases where you need the actual content (to search, display custom-styled, or process), the full OAuth + API route approach is the right path.

## Before you start

- A Google account with access to Google Cloud Console (console.cloud.google.com)
- A Google Cloud project with the Google Docs API enabled
- OAuth 2.0 credentials (Client ID and Client Secret) configured for a web application with your deployed callback URL
- A deployed Bolt.new app on Netlify or Bolt Cloud (required for the OAuth redirect URI — the auth flow cannot run in the WebContainer preview)
- A Next.js project in Bolt with the googleapis npm package (prompt: 'Install the googleapis npm package')

## Step-by-step guide

### 1. Enable the Google Docs API and create OAuth 2.0 credentials

Start in the Google Cloud Console. If you do not have a project, create one at console.cloud.google.com by clicking 'New Project'. Give it a name and click Create.

With your project selected, navigate to APIs & Services → Library. Search for 'Google Docs API' and click Enable. Also enable the Google Drive API — you will need it to list documents and set sharing permissions on newly created docs.

Next, create OAuth 2.0 credentials. Go to APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID. If prompted, configure the OAuth consent screen first: choose 'External' for testing (allows any Google account), fill in the app name, support email, and developer email. On the Scopes page, add https://www.googleapis.com/auth/documents for Docs access and https://www.googleapis.com/auth/drive.file for creating and managing files your app creates.

Back in Create Credentials, select 'Web application' as the application type. In Authorized redirect URIs, add your deployed app's callback URL (e.g., https://your-app.netlify.app/api/google/callback) and http://localhost:3000/api/google/callback for local testing. Click Create and copy the Client ID and Client Secret.

Store these in your .env.local file. The Google OAuth client secret must only ever appear in server-side code — never in React components or any code that runs in the browser.

```
// .env.local
GOOGLE_CLIENT_ID=your_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_REDIRECT_URI=https://your-app.netlify.app/api/google/callback

// lib/google-auth.ts
import { google } from 'googleapis';

export function getOAuthClient() {
  return new google.auth.OAuth2(
    process.env.GOOGLE_CLIENT_ID,
    process.env.GOOGLE_CLIENT_SECRET,
    process.env.GOOGLE_REDIRECT_URI
  );
}

export function getAuthUrl(): string {
  const oauth2Client = getOAuthClient();
  return oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: [
      'https://www.googleapis.com/auth/documents',
      'https://www.googleapis.com/auth/drive.file',
    ],
    prompt: 'consent', // Force refresh token on each auth
  });
}
```

**Expected result:** Visiting /api/google/authorize redirects to Google's consent screen showing your app name and the requested Docs/Drive scopes. After authorizing, you are redirected back to your callback URL.

### 2. Implement the OAuth callback and token storage

After the user authorizes your app on Google's consent screen, Google redirects them to your callback URL with an authorization code as a query parameter. Your callback route must exchange this code for access and refresh tokens by calling Google's token endpoint, then store the tokens so your app can make authenticated Docs API calls.

The googleapis library handles the token exchange and management. The OAuth2 client's getToken method exchanges the code for a credentials object containing access_token, refresh_token, expiry_date, and token_type. The refresh_token is particularly important — it allows your server to get new access tokens when the current one expires, without requiring the user to re-authorize.

For token storage, the approach depends on your app's complexity. For a single-user tool or prototype, storing tokens in an encrypted HTTP-only cookie is simple and effective. For a multi-user app where different users connect their own Google accounts, store tokens in a database keyed by your user's ID. Never store refresh tokens in environment variables — they must be persisted across server restarts and may need to be updated when refreshed.

An important constraint: the OAuth callback is an incoming redirect from Google to your server. Bolt's WebContainer cannot receive these incoming connections. Deploy your app first, register the deployed URL as the redirect URI in Google Cloud Console, and test the full auth flow on the deployed site. During development, you can temporarily use the localhost redirect URI to test the callback locally by running Next.js outside of Bolt.

```
// app/api/google/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getOAuthClient } from '@/lib/google-auth';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const error = searchParams.get('error');

  if (error) {
    return NextResponse.redirect(new URL(`/error?message=${encodeURIComponent(error)}`, request.url));
  }

  if (!code) {
    return NextResponse.json({ error: 'Missing authorization code' }, { status: 400 });
  }

  try {
    const oauth2Client = getOAuthClient();
    const { tokens } = await oauth2Client.getToken(code);

    // Store tokens in an HTTP-only cookie
    // In production, encrypt this or store in a database keyed by user ID
    const tokenData = JSON.stringify({
      access_token: tokens.access_token,
      refresh_token: tokens.refresh_token,
      expiry_date: tokens.expiry_date,
      token_type: tokens.token_type,
    });

    const response = NextResponse.redirect(new URL('/dashboard', request.url));
    response.cookies.set('google_tokens', tokenData, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 30, // 30 days
      path: '/',
    });

    return response;
  } catch (err) {
    console.error('Token exchange error:', err);
    return NextResponse.redirect(new URL('/error?message=auth_failed', request.url));
  }
}

// lib/get-google-client.ts — helper to get authenticated client from cookie
import { cookies } from 'next/headers';

export async function getAuthenticatedClient() {
  const cookieStore = cookies();
  const tokenCookie = cookieStore.get('google_tokens');

  if (!tokenCookie) throw new Error('Not authenticated with Google');

  const tokens = JSON.parse(tokenCookie.value);
  const oauth2Client = getOAuthClient();
  oauth2Client.setCredentials(tokens);
  return oauth2Client;
}
```

**Expected result:** After completing the OAuth flow on the deployed app, the browser redirects to /dashboard and a google_tokens HTTP-only cookie is set. Subsequent API routes can retrieve this cookie to make authenticated Docs API calls.

### 3. Create and read Google Docs via API routes

With authentication working, you can now create documents and read their content. The Google Docs API is accessed through the googleapis library's google.docs() client. Every API call needs an authenticated OAuth2 client — use the getAuthenticatedClient helper from the previous step.

Creating a document is a single API call to documents.create with a title. This creates an empty document and returns the document ID, which is the same string you see in the Google Docs URL (/document/d/{documentId}/edit). Once you have a document ID, you can insert content using batchUpdate, set permissions using the Drive API, and get the document URL.

Reading document content uses documents.get, which returns the complete document structure as a nested JSON object. The body.content array contains structural elements: paragraphs, tables, and section breaks. Each paragraph has a paragraphStyle indicating whether it is a heading, normal text, title, or list item. Within paragraphs, textRun elements contain the actual text content and formatting (bold, italic, foreground color, font size). Parsing this structure to display as HTML requires traversing the element tree and mapping structural types to HTML elements.

For the most common use case — replacing template placeholders with dynamic values — use batchUpdate with replaceAllText requests. This is far simpler than calculating character offsets. Define placeholder strings in your template document (e.g., {{CLIENT_NAME}}, {{DATE}}, {{PROJECT_SCOPE}}) and batch-replace them all with actual values in a single API call.

```
// app/api/docs/create/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { google } from 'googleapis';
import { getAuthenticatedClient } from '@/lib/get-google-client';

export async function POST(request: NextRequest) {
  try {
    const { title, templateId } = await request.json() as {
      title: string;
      templateId?: string;
    };

    const auth = await getAuthenticatedClient();

    if (templateId) {
      // Copy an existing template document
      const drive = google.drive({ version: 'v3', auth });
      const copy = await drive.files.copy({
        fileId: templateId,
        requestBody: { name: title },
      });
      const docId = copy.data.id!;
      return NextResponse.json({
        documentId: docId,
        editUrl: `https://docs.google.com/document/d/${docId}/edit`,
        viewUrl: `https://docs.google.com/document/d/${docId}/view`,
      });
    }

    // Create a new empty document
    const docs = google.docs({ version: 'v1', auth });
    const doc = await docs.documents.create({
      requestBody: { title },
    });

    const docId = doc.data.documentId!;
    return NextResponse.json({
      documentId: docId,
      editUrl: `https://docs.google.com/document/d/${docId}/edit`,
      viewUrl: `https://docs.google.com/document/d/${docId}/view`,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Failed to create document';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}

// Replace template placeholders in a document
// app/api/docs/[docId]/replace/route.ts
export async function POST_replace(request: NextRequest, { params }: { params: { docId: string } }) {
  const { replacements } = await request.json() as {
    replacements: Record<string, string>;
  };
  const auth = await getAuthenticatedClient();
  const docs = google.docs({ version: 'v1', auth });

  const requests = Object.entries(replacements).map(([placeholder, value]) => ({
    replaceAllText: {
      containsText: { text: `{{${placeholder}}}`, matchCase: true },
      replaceText: value,
    },
  }));

  await docs.documents.batchUpdate({
    documentId: params.docId,
    requestBody: { requests },
  });

  return NextResponse.json({ success: true });
}
```

**Expected result:** POST /api/docs/create with { title: 'Q4 Report' } creates a new Google Doc and returns its documentId and edit URL. Opening the edit URL in a browser shows the document in Google Docs.

### 4. Embed Google Docs for simple read-only display

For use cases where you only need to display a Google Doc within your app without editing or extracting content, iframe embedding is much simpler than the full OAuth integration. Google provides a dedicated embed URL for any document that has been shared with 'Anyone with the link can view' permissions.

The embed URL format is: https://docs.google.com/document/d/{documentId}/pub?embedded=true. This renders the document content as clean HTML in an iframe, with Google's styling applied. No API key, no OAuth, no server-side code needed — just a React component with an iframe element.

This approach is perfect for: public documentation pages, help articles maintained in Google Docs, terms of service or privacy policy pages, or any content where your team writes in Google Docs and wants to display it live in your app without a CMS migration. When someone updates the doc in Google Docs, the embedded version updates automatically.

The limitations are meaningful: you cannot extract or restyle the text content, the iframe shows Google's formatting (which you cannot override), and the embed URL only works for publicly shared documents. Private documents require the OAuth flow. Also note that the embedded Docs view does not render the document's print layout — it renders a web-optimized version that may look slightly different from the editor view.

For Bolt's WebContainer preview, iframe embedding works without any configuration — just drop in the embed URL. This is the fastest way to get Google Docs content into a Bolt app during initial prototyping.

```
// components/GoogleDocEmbed.tsx
'use client';

import { useState } from 'react';

interface GoogleDocEmbedProps {
  /** Google Document ID from the URL: /document/d/{documentId}/edit */
  documentId: string;
  /** Minimum height of the embed iframe in pixels. Default: 600 */
  minHeight?: number;
  /** Show the 'Open in Google Docs' link. Default: true */
  showOpenLink?: boolean;
}

/**
 * Embeds a Google Doc as an iframe using the published embed URL.
 * REQUIREMENT: The document must have sharing set to
 * 'Anyone with the link can view' in Google Docs share settings.
 */
export default function GoogleDocEmbed({
  documentId,
  minHeight = 600,
  showOpenLink = true,
}: GoogleDocEmbedProps) {
  const [isLoaded, setIsLoaded] = useState(false);

  const embedUrl = `https://docs.google.com/document/d/${documentId}/pub?embedded=true`;
  const editUrl = `https://docs.google.com/document/d/${documentId}/edit`;

  return (
    <div className="relative w-full border rounded-lg overflow-hidden">
      {!isLoaded && (
        <div
          className="absolute inset-0 flex items-center justify-center bg-gray-50"
          style={{ minHeight: `${minHeight}px` }}
        >
          <div className="text-gray-400 flex flex-col items-center gap-2">
            <div className="w-8 h-8 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
            <p className="text-sm">Loading document...</p>
          </div>
        </div>
      )}
      <iframe
        src={embedUrl}
        onLoad={() => setIsLoaded(true)}
        style={{ minHeight: `${minHeight}px`, width: '100%', border: 'none' }}
        title="Google Document"
        sandbox="allow-scripts allow-same-origin"
      />
      {showOpenLink && (
        <div className="border-t px-4 py-2 bg-gray-50 flex justify-end">
          <a
            href={editUrl}
            target="_blank"
            rel="noopener noreferrer"
            className="text-sm text-blue-600 hover:underline"
          >
            Open in Google Docs →
          </a>
        </div>
      )}
    </div>
  );
}
```

**Expected result:** The GoogleDocEmbed component renders the Google Doc content inside your Bolt app. The document text, headings, and formatting are visible in the preview, and the 'Open in Google Docs' link opens the document editor in a new tab.

## Best practices

- Always deploy before testing the OAuth flow — Google's redirect URI must be a publicly accessible HTTPS URL that the WebContainer cannot provide during development.
- Store OAuth tokens in a database for multi-user apps, not in cookies or environment variables. Tokens expire and refresh, so they need to be updatable at runtime.
- Use replaceAllText in batchUpdate for template-based document generation — it is simpler and more reliable than calculating character offsets for insertions.
- Request only the scopes your app needs: use https://www.googleapis.com/auth/documents for Docs access and https://www.googleapis.com/auth/drive.file to only access files your app creates, rather than the broader drive scope.
- For read-only public content display, prefer iframe embedding over the API — it requires no authentication setup and automatically reflects document updates.
- Cache read API responses when document content does not change frequently. The Docs API has per-user rate limits; excessive reads on the same documents can trigger 429 errors.
- Never expose your Google Client Secret or OAuth tokens in client-side React code. All Google API calls must go through server-side Next.js API routes.

## Use cases

### Contract generation from form data

When a user fills out a form in your Bolt app (client name, project scope, pricing), automatically create a Google Doc using a template document, replacing placeholder text with the actual values. The generated document is ready to share with the client for signing.

Prompt example:

```
Create a Next.js app that generates Google Docs contracts. When a user submits a form with client name, project description, and total price, call the Google Docs API to create a new document based on a template document ID. Replace the placeholders {{CLIENT_NAME}}, {{PROJECT_DESCRIPTION}}, and {{TOTAL_PRICE}} using the Docs API batchUpdate with replaceAllText requests. Return the new document's URL.
```

### Document content viewer with custom styling

Build a custom document viewer that fetches a Google Doc's content through the API and renders it with your app's own typography and styling, rather than showing it in Google's iframe. This is useful for help centers, knowledge bases, or content platforms where you want consistent branding.

Prompt example:

```
Build a Google Docs content viewer that fetches document content using the Google Docs API and renders it as styled React components. Parse the document's body elements — paragraphs, headings (HEADING_1 through HEADING_6), and lists — and render them with Tailwind CSS typography classes. Handle bold, italic, and link formatting within text runs.
```

### Meeting notes auto-population

Before a meeting, automatically create a Google Doc with the meeting agenda, attendee list, and action items sections pre-populated from your app's meeting data. The document link is sent to participants so everyone has a structured notes template ready when the meeting starts.

Prompt example:

```
Create an API route that creates a meeting notes Google Doc. Accept meeting title, date, attendees array, and agenda items array as input. Create a new Google Doc with the title as the document title, a formatted attendees list as a table, agenda items as a numbered list, and empty sections for Notes and Action Items. Return the document ID and shareable link.
```

## Troubleshooting

### OAuth callback returns 'redirect_uri_mismatch' error from Google

Cause: The redirect URI sent in the authorization request does not exactly match one of the URIs registered in Google Cloud Console. Google performs a strict string comparison including protocol, path, and trailing slashes.

Solution: In Google Cloud Console → APIs & Services → Credentials, open your OAuth 2.0 Client ID and verify the Authorized redirect URIs. Copy one exactly (character for character) into your GOOGLE_REDIRECT_URI environment variable. Common mismatches: http vs https, missing or extra trailing slash, localhost vs 127.0.0.1.

### OAuth callback works but subsequent API calls return 401 after one hour

Cause: The access token has expired and the refresh token is not being used to obtain a new one. This happens when access_type was not set to 'offline' in the auth URL, so no refresh token was issued.

Solution: Regenerate the auth URL with access_type: 'offline' and prompt: 'consent'. The user must re-authorize to receive a new refresh token. After that, the googleapis library automatically uses the refresh token when the access token expires, provided you call oauth2Client.setCredentials(tokens) before each API call.

```
// Correct auth URL generation:
const authUrl = oauth2Client.generateAuthUrl({
  access_type: 'offline',  // Required for refresh token
  prompt: 'consent',       // Forces new refresh token even if already authorized
  scope: ['https://www.googleapis.com/auth/documents'],
});
```

### Google OAuth login page appears in the Bolt preview but after authorizing, the page shows a 404 or no redirect happens

Cause: The OAuth callback URL points to your deployed app's domain, but the Bolt preview runs on a different URL. Intuit redirects to the registered domain, which the WebContainer cannot intercept.

Solution: Deploy your app to Netlify or Bolt Cloud first. Register the deployed HTTPS URL (e.g., https://your-app.netlify.app/api/google/callback) in Google Cloud Console. Test the complete auth flow on the deployed site. For local development outside Bolt, add http://localhost:3000/api/google/callback as a second authorized redirect URI.

### documents.get returns document structure but extracting text is unclear — nested objects everywhere

Cause: The Google Docs API document model is a deeply nested tree of structural elements, not a flat text string. Each paragraph contains elements, each element may be a textRun with content or an inlineObjectElement for images.

Solution: Use a recursive extraction pattern that traverses the body.content array and extracts text from nested textRun.content fields. The helper function below extracts all text content as a plain string from any Google Docs API response.

```
function extractText(doc: GoogleDocsDocument): string {
  const lines: string[] = [];
  for (const element of doc.body?.content ?? []) {
    if (element.paragraph) {
      const text = element.paragraph.elements
        ?.map((el) => el.textRun?.content ?? '')
        .join('');
      if (text?.trim()) lines.push(text.replace(/\n$/, ''));
    }
  }
  return lines.join('\n');
}
```

## Frequently asked questions

### Can I test the Google Docs API in Bolt's preview without deploying?

Partially. Outbound API calls to read and create documents work fine once you have a valid access token. However, the OAuth flow — where Google redirects back to your app after authorization — requires a deployed URL that the WebContainer cannot provide. Deploy first to complete auth, obtain tokens, then you can use those tokens to test most API operations in development.

### Does Bolt.new have a native Google Docs integration?

No. Google Docs is not one of Bolt's native connectors. You implement the integration using Next.js API routes and the googleapis npm package. Bolt's AI generates most of the boilerplate code when prompted, but you need to set up credentials in Google Cloud Console manually.

### How do I allow users to connect their own Google account rather than using my service account?

The OAuth 2.0 flow described in this guide is exactly the mechanism for user-specific access. Each user who authorizes your app receives their own tokens, stored in your database associated with their user ID. When they make requests, use their stored tokens to call the Docs API on their behalf. This is the standard pattern for apps that read or edit documents in users' own Google Drive.

### What is the Google Docs API rate limit?

The Google Docs API allows 300 read requests per minute and 60 write requests per minute per user. For a dashboard accessed by many users simultaneously, each user has their own quota so the total throughput scales. If you hit rate limits (429 error), implement exponential backoff retry logic. Caching frequently-read documents is the most effective way to stay within limits.

### Can I insert images into Google Docs through the API?

Yes, using the insertInlineImage request in batchUpdate. You provide a URI pointing to a publicly accessible image (Google fetches it from the URL you provide) and the insertion location index within the document. The image must be accessible over HTTPS without authentication — you cannot reference private S3 or local file system paths.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/google-docs
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/google-docs
