To use JIRA with V0 by Vercel, call the Atlassian JIRA REST API from Next.js API routes using Basic Auth or OAuth 2.0. Store your Atlassian API token as a server-only Vercel environment variable. V0 generates your project tracking dashboard UI; API routes fetch issues, sprints, and boards securely from your JIRA instance.
Building Custom JIRA Dashboards with V0 and Next.js
JIRA is the standard issue tracker for most software development teams, but its default interface can feel overwhelming for non-engineering stakeholders. With V0 and the JIRA REST API, you can build custom project views that show exactly what your team needs — a clean sprint board for standup meetings, a simplified issue table for product managers, or a velocity chart for engineering leaders — without navigating JIRA's full complexity.
The integration works through JIRA's REST API v3, which is available on both Jira Cloud and Jira Server. Authentication uses Basic Auth with your Atlassian account email and an API token generated from your Atlassian account settings. Because API tokens are sensitive credentials, all JIRA API calls must happen through Next.js API routes — never from client-side React components. Vercel's serverless functions handle the credential management and serve clean JSON to your V0-generated UI.
V0 is well-suited for generating issue tracking interfaces: data tables with status badges, kanban-style sprint boards, filter panels, and summary stat cards. This tutorial covers fetching issues with JQL (JIRA Query Language), displaying sprint data, creating issues from a form, and deploying the complete stack to Vercel.
Integration method
V0 generates the React UI — sprint boards, issue tables, and backlog views. Next.js API routes in your Vercel-deployed app call the JIRA REST API v3 using Basic Auth with an Atlassian API token. All credentials stay server-side in Vercel environment variables; your React components fetch data from your own API routes rather than calling Atlassian directly.
Prerequisites
- A V0 account at v0.dev and a Next.js project to work with
- A JIRA Cloud account at atlassian.net (Jira Server also works with a different base URL)
- An Atlassian API token generated at id.atlassian.com/manage-profile/security/api-tokens
- Your JIRA project key (visible in issue IDs like 'PROJ-123', the 'PROJ' part)
- A Vercel account for deployment and environment variable management
Step-by-step guide
Generate an Atlassian API Token
Generate an Atlassian API Token
JIRA's REST API uses Basic Authentication: your Atlassian account email combined with an API token. API tokens are separate from your account password and can be revoked independently — always use a token rather than your actual password. To create one, go to id.atlassian.com/manage-profile/security/api-tokens (you must be logged in to your Atlassian account). Click Create API token, give it a descriptive label like 'V0 Dashboard Integration', and click Create. Copy the token immediately — Atlassian only shows it once. Store it somewhere secure like 1Password or your password manager. The token is used in Basic Auth as a base64-encoded string: your email address as the username and the API token as the password. In your Next.js API routes, you'll construct the Authorization header as: Authorization: Basic base64(email:apiToken). Also note your JIRA Cloud base URL — it looks like https://your-company.atlassian.net. For Jira Server, your URL is your organization's self-hosted domain. You'll also need your project key — look at any issue in your project and note the prefix before the dash (e.g., in 'PROJ-123', the project key is PROJ). With these three pieces — email, API token, and base URL — you can authenticate against any JIRA REST API v3 endpoint.
Pro tip: Create dedicated API tokens per integration rather than reusing one token across all your tools. This way you can revoke just the JIRA dashboard token if it's ever compromised, without affecting other Atlassian integrations.
Expected result: An Atlassian API token has been generated and copied. You have your Atlassian email, API token, and JIRA base URL ready to add as Vercel environment variables.
Generate the Project Dashboard UI in V0
Generate the Project Dashboard UI in V0
Open V0 at v0.dev and describe the JIRA project interface you want to build. JIRA data revolves around issues, which have fields like key (PROJ-123), summary (the title), status, priority, assignee, issue type, and story points. Sprint data adds a layer with sprint name, start date, end date, and sprint goal. Describe your desired UI to V0 using these field names — issue cards with the key badge, status chips color-coded by status name, priority icons. Ask V0 to generate the component with mock data that mimics JIRA's response structure: an array of issues where each has an id, key, fields object containing summary, status.name, priority.name, assignee.displayName, and story_points. Using this structure in your mock data means the live component integration is a direct drop-in — no data transformation needed beyond mapping the JIRA REST API response. Use Design Mode to polish colors and spacing. Sprint board layouts work especially well with V0's layout generation. Request a header row showing sprint name and dates, columns for each status, and scrollable issue card stacks within columns.
Create a sprint board page with a header showing Sprint Name, start date, end date, and a circular progress indicator showing percentage complete. Below, show four columns: To Do, In Progress, In Review, Done. Each column header shows a count badge. Issue cards inside each column show: issue key as a blue badge, issue summary, assignee name, priority as an icon (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low), and story points in a gray circle. Mock data should use {id, key, fields: {summary, status: {name}, priority: {name}, assignee: {displayName}, story_points}} shape.
Paste this in V0 chat
Pro tip: JIRA's REST API returns issue data in a deeply nested structure — fields are inside a fields object. Designing your mock data to match this shape (issue.fields.summary, issue.fields.status.name) saves you from adding a mapping layer later.
Expected result: A styled sprint board UI renders in V0's preview with mock JIRA-shaped data. Columns, issue cards, and the sprint header look complete and match the expected JIRA data structure.
Create JIRA API Routes for Issues and Sprints
Create JIRA API Routes for Issues and Sprints
Now build the Next.js API routes that call JIRA's REST API. JIRA Cloud uses the base URL https://your-company.atlassian.net/rest/api/3/ for core issue operations and https://your-company.atlassian.net/rest/agile/1.0/ for sprint and board data. Authentication is Basic Auth — encode your email and API token as base64 and pass it in the Authorization header. Create a utility function in lib/jira.ts that builds the authorization header and provides a typed fetch wrapper. Your main routes are: GET /api/jira/issues — uses JQL (JIRA Query Language) to query issues. JQL is a SQL-like query language: 'project = PROJ AND sprint in openSprints() ORDER BY priority ASC'. GET /api/jira/sprints/active — calls the Agile API to get the active sprint for a board. POST /api/jira/issues — creates a new issue with summary, description, issue type, and priority. The JIRA REST API returns paginated results with startAt, maxResults, and total fields for issue searches. Handle pagination in your API routes by passing startAt and maxResults as query parameters from your frontend. For JQL queries, URL-encode the query string before passing it as a query parameter. JIRA's error responses include a detailed errors object — log these in your API route's catch block for easier debugging.
Create a Next.js API route at app/api/jira/issues/route.ts. On GET requests, accept a jql query param and fetch from the JIRA REST API v3 search endpoint using Basic Auth (email + API token from env vars). Return the issues array with fields: key, summary, status.name, priority.name, assignee.displayName, story_points. On POST requests, create a new issue from the JSON body. Handle errors and return appropriate status codes.
Paste this in V0 chat
1// lib/jira.ts2const JIRA_BASE_URL = process.env.JIRA_BASE_URL; // e.g., https://company.atlassian.net3const JIRA_EMAIL = process.env.JIRA_EMAIL;4const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;56function getAuthHeader(): string {7 const credentials = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');8 return `Basic ${credentials}`;9}1011export async function jiraFetch(path: string, options?: RequestInit) {12 const res = await fetch(`${JIRA_BASE_URL}/rest/api/3${path}`, {13 ...options,14 headers: {15 Authorization: getAuthHeader(),16 'Content-Type': 'application/json',17 Accept: 'application/json',18 ...options?.headers,19 },20 });21 if (!res.ok) {22 const error = await res.json();23 throw new Error(JSON.stringify(error));24 }25 return res.json();26}2728// app/api/jira/issues/route.ts29import { NextRequest, NextResponse } from 'next/server';30import { jiraFetch } from '@/lib/jira';3132export async function GET(req: NextRequest) {33 const jql = req.nextUrl.searchParams.get('jql') ||34 `project = ${process.env.JIRA_PROJECT_KEY} AND sprint in openSprints() ORDER BY priority ASC`;3536 try {37 const data = await jiraFetch(38 `/search?jql=${encodeURIComponent(jql)}&fields=summary,status,priority,assignee,story_points,issuetype&maxResults=50`39 );40 return NextResponse.json({ issues: data.issues, total: data.total });41 } catch (error) {42 return NextResponse.json({ error: 'Failed to fetch JIRA issues' }, { status: 500 });43 }44}4546export async function POST(req: NextRequest) {47 try {48 const body = await req.json();49 const issue = await jiraFetch('/issue', {50 method: 'POST',51 body: JSON.stringify({52 fields: {53 project: { key: process.env.JIRA_PROJECT_KEY },54 summary: body.summary,55 description: body.description ? {56 type: 'doc',57 version: 1,58 content: [{ type: 'paragraph', content: [{ type: 'text', text: body.description }] }]59 } : undefined,60 issuetype: { name: body.issuetype || 'Story' },61 priority: { name: body.priority || 'Medium' },62 },63 }),64 });65 return NextResponse.json({ key: issue.key, id: issue.id }, { status: 201 });66 } catch (error) {67 return NextResponse.json({ error: 'Failed to create issue' }, { status: 500 });68 }69}Pro tip: JIRA's description field uses Atlassian Document Format (ADF) — a JSON structure, not plain text. When creating issues, wrap the description in the ADF doc structure shown in the code above or leave it undefined for simple issue creation.
Expected result: API routes at /api/jira/issues respond with real JIRA data. GET returns an issues array matching the mock data shape used in your V0 component. POST creates new issues in your JIRA project.
Connect V0 Components to JIRA Data and Handle Updates
Connect V0 Components to JIRA Data and Handle Updates
Update your V0-generated sprint board or issue table to fetch from your JIRA API routes. Since the sprint board benefits from filtering and user interaction — clicking columns, filtering by assignee — mark these components as client components with 'use client'. Fetch from /api/jira/issues on mount with the default JQL for the current sprint. Let users change the JQL filter by adding a filter bar with preset buttons like 'My Issues', 'High Priority', and 'Bugs'. When a preset is clicked, append the new JQL as a query parameter to your fetch call. For issue creation, build a modal form that POSTs to /api/jira/issues with summary, issue type, and priority. Show optimistic updates in the UI — add the new issue to local state immediately with a 'Creating...' status, then replace it with the real response once the API call succeeds. For drag-and-drop status updates (moving cards between sprint columns), implement a PUT route at /api/jira/issues/[id]/transition that calls JIRA's transition endpoint to move an issue to a different status. JIRA transitions are workflow-specific — you need to fetch available transitions for an issue first, then trigger the correct transition ID. Handle loading and error states thoroughly since JIRA API calls can be slow (200–800ms) depending on your org size.
Update the sprint board component to fetch issues from /api/jira/issues on mount. Store issues in useState. Group them by status (fields.status.name) to populate the kanban columns. Add a New Issue button that opens a modal with inputs for Summary, Issue Type (dropdown: Story/Bug/Task), and Priority (dropdown). On form submit, POST to /api/jira/issues and add the returned issue to local state. Show a toast notification on success or failure.
Paste this in V0 chat
1'use client';23import { useEffect, useState } from 'react';45interface JiraIssue {6 id: string;7 key: string;8 fields: {9 summary: string;10 status: { name: string };11 priority: { name: string };12 assignee: { displayName: string } | null;13 issuetype: { name: string };14 };15}1617const COLUMNS = ['To Do', 'In Progress', 'In Review', 'Done'];1819export default function SprintBoard() {20 const [issues, setIssues] = useState<JiraIssue[]>([]);21 const [loading, setLoading] = useState(true);2223 useEffect(() => {24 fetch('/api/jira/issues')25 .then((r) => r.json())26 .then((data) => { setIssues(data.issues || []); setLoading(false); })27 .catch(() => setLoading(false));28 }, []);2930 const byStatus = (status: string) =>31 issues.filter((i) => i.fields.status.name === status);3233 if (loading) return <div className="p-8 text-center">Loading sprint data...</div>;3435 return (36 <div className="grid grid-cols-4 gap-4 p-4">37 {COLUMNS.map((col) => (38 <div key={col} className="bg-gray-50 rounded-lg p-3">39 <h3 className="font-semibold mb-3">{col} ({byStatus(col).length})</h3>40 {byStatus(col).map((issue) => (41 <div key={issue.id} className="bg-white rounded p-3 mb-2 shadow-sm">42 <span className="text-xs text-blue-600 font-mono">{issue.key}</span>43 <p className="text-sm mt-1">{issue.fields.summary}</p>44 <div className="flex justify-between mt-2 text-xs text-gray-500">45 <span>{issue.fields.priority.name}</span>46 <span>{issue.fields.assignee?.displayName ?? 'Unassigned'}</span>47 </div>48 </div>49 ))}50 </div>51 ))}52 </div>53 );54}Pro tip: JIRA status names are case-sensitive and may differ from your expected values (e.g., 'In Progress' vs 'In-Progress'). Log a few raw API responses to confirm the exact status names used in your specific JIRA workflow before filtering by them.
Expected result: The sprint board component renders live JIRA data grouped by status. Issues appear in the correct columns based on their JIRA workflow status. New issues created via the form appear immediately in the To Do column.
Set Environment Variables in Vercel and Deploy
Set Environment Variables in Vercel and Deploy
JIRA credentials should be stored as server-only environment variables — no NEXT_PUBLIC_ prefix. Add four variables to your Vercel project: JIRA_BASE_URL (your Atlassian site URL, e.g., https://your-company.atlassian.net — no trailing slash), JIRA_EMAIL (your Atlassian account email address used for API token authentication), JIRA_API_TOKEN (the token you generated at id.atlassian.com), and JIRA_PROJECT_KEY (your project's key like 'PROJ' or 'DEV'). To add these in Vercel, go to your project page in the Vercel Dashboard, click Settings → Environment Variables, and add each key-value pair. Apply them to Production and Preview environments. For local development, add the same variables to your .env.local file. After saving all variables, push your code to GitHub to trigger a Vercel deployment. Once deployed, open the production URL and verify that the sprint board or issue table loads real JIRA data. If issues don't appear, check the Vercel Function Logs under your project's Functions tab — common issues are wrong base URLs (missing https://, trailing slashes) and base64 encoding errors in the auth header. Test issue creation via the New Issue form to confirm write access is working.
1# .env.local — never commit this file2# JIRA credentials (all server-only, no NEXT_PUBLIC_ prefix)3JIRA_BASE_URL=https://your-company.atlassian.net4JIRA_EMAIL=you@yourcompany.com5JIRA_API_TOKEN=ATATTx...6JIRA_PROJECT_KEY=PROJPro tip: The JIRA_BASE_URL must NOT have a trailing slash (no https://company.atlassian.net/). The jiraFetch utility in lib/jira.ts already appends /rest/api/3 — a trailing slash in the base URL creates double-slash URLs that the Atlassian API rejects.
Expected result: All JIRA environment variables are set in Vercel. The deployed app fetches and displays live JIRA data. Issue creation from the UI creates real issues in your JIRA project.
Common use cases
Sprint Progress Dashboard
Build a focused sprint dashboard that shows only the current sprint's issues, grouped by assignee or status. Pull the active sprint from the JIRA Agile API, then fetch issues in that sprint with JQL. Display a progress bar showing what percentage of story points are complete, making standup meetings faster.
Create a sprint dashboard with a header showing Sprint Name, Sprint Goal, and a progress bar (percentage of issues Done). Below show a kanban-style column layout with To Do, In Progress, In Review, and Done columns. Each issue card should show the issue key, summary, assignee avatar, priority icon, and story points. Fetch data from /api/jira/sprints/active.
Copy this prompt to try it in V0
Bug Tracking Report
Build a dedicated bug reporting interface that displays all open bugs filtered by priority, assignee, or affected version. Stakeholders can see high-priority bugs without logging into JIRA. Use JQL to query for bugs specifically: issuetype = Bug AND status != Done.
Build a bug tracking table with columns: Issue Key (linked), Summary, Priority (colored label: Critical/High/Medium/Low), Assignee, Status (badge), and Days Open. Add filter buttons at the top for Priority and Status. Show a summary bar at the top: Total Bugs, Critical count, Unassigned count. Fetch from /api/jira/issues?jql=issuetype=Bug AND status!=Done.
Copy this prompt to try it in V0
Product Backlog Manager
Create a simplified backlog view for product managers to triage and prioritize issues without the complexity of JIRA's full backlog UI. Display issues by epic, allow updating priority from a dropdown, and create new issues directly from the interface.
Create a backlog management page with a list of epics on the left sidebar. Clicking an epic shows its child issues in the main panel as a sortable list with columns: Key, Summary, Status, Priority, Story Points, and Epic Link. Add a New Issue button that opens a modal form with fields for Summary, Description, Issue Type, and Priority. POST to /api/jira/issues to create.
Copy this prompt to try it in V0
Troubleshooting
API route returns 401 Unauthorized when calling JIRA REST API
Cause: The Basic Auth header is malformed, the API token is incorrect, or the JIRA_EMAIL and JIRA_API_TOKEN environment variables are missing or misspelled in Vercel.
Solution: Verify that both JIRA_EMAIL and JIRA_API_TOKEN are set correctly in Vercel Settings → Environment Variables. Confirm you're using the API token (not your Atlassian password). Regenerate the token at id.atlassian.com if unsure. In your API route, log the auth header (temporarily) to confirm the base64 encoding is correct.
1// Test the auth header manually2const credentials = Buffer.from(`${process.env.JIRA_EMAIL}:${process.env.JIRA_API_TOKEN}`).toString('base64');3console.log('Auth header:', `Basic ${credentials}`);4// Should look like: Basic dXNlckBleGFtcGxlLmNvbTphcGl0b2tlbg==JQL query returns 400 Bad Request with 'The value X does not exist for the field project'
Cause: The JIRA project key in your JQL query or JIRA_PROJECT_KEY environment variable doesn't match an actual project key in your JIRA org, or the API user doesn't have access to that project.
Solution: Go to your JIRA project and look at any issue key — the letters before the dash are your project key (e.g., 'MYAPP' in 'MYAPP-42'). Update JIRA_PROJECT_KEY to match exactly, including case. Verify that the Atlassian account tied to your API token has at least 'Browse Projects' permission in that project.
Issues appear in the API response but the sprint board shows wrong columns because status names don't match
Cause: JIRA workflows are customizable — your project's status names may differ from generic names like 'To Do' or 'In Progress'. V0 generates column names that don't match your actual JIRA workflow statuses.
Solution: Fetch a sample issue and log its fields.status.name to see the exact status name strings in your workflow. Update your component's COLUMNS array to match. For projects with custom workflows, fetch available statuses from /rest/api/3/project/{projectKey}/statuses and use those values dynamically.
1// Log actual status names from your first few issues2console.log(data.issues.slice(0, 3).map((i: JiraIssue) => i.fields.status.name));3// Then update the COLUMNS array to match your actual workflowCreating issues via POST fails with 'Field 'description' cannot be set' error
Cause: JIRA Cloud requires descriptions in Atlassian Document Format (ADF) — a JSON structure. Sending a plain text string for the description field will fail.
Solution: Wrap the description text in ADF format as shown in the issue creation code, or simply omit the description field for basic issue creation and let users add descriptions directly in JIRA after the issue is created.
1// Correct ADF format for description2description: {3 type: 'doc',4 version: 1,5 content: [{6 type: 'paragraph',7 content: [{ type: 'text', text: yourDescriptionString }]8 }]9}Best practices
- Always use API tokens for JIRA authentication — never use your Atlassian account password as it creates a security risk and doesn't support token rotation
- Build specific JQL queries with project and sprint filters rather than fetching all issues — unbounded queries can timeout on large JIRA instances with thousands of issues
- Store JIRA_PROJECT_KEY as an environment variable rather than hardcoding it, so you can point different Vercel environments at different JIRA projects (staging vs. production projects)
- Cache JIRA API responses for 30–60 seconds using Next.js fetch cache or Vercel's built-in caching — JIRA's API can be slow and boards don't need millisecond freshness
- Handle JIRA's pagination fields (startAt, maxResults, total) in your API routes — large projects can return 50 issues per page; implement load-more or pagination in your V0 components
- Use the /rest/api/3/myself endpoint to verify your credentials are working before building complex queries
- For issue transitions (changing status via drag-and-drop), fetch available transitions per issue using /rest/api/3/issue/{id}/transitions before calling the transition endpoint — transition IDs are workflow-specific
Alternatives
ClickUp has a more developer-friendly REST API with simpler authentication and a free tier that includes API access, making it faster to integrate than JIRA for smaller teams.
Notion databases can serve as a lightweight project tracker with a clean REST API, though it lacks JIRA's sprint management and agile-specific features.
Teamwork offers project management with a simpler API and lower pricing than JIRA, suitable for teams that don't need JIRA's enterprise-scale agile tooling.
Frequently asked questions
Does V0 have a native JIRA integration or Marketplace connector?
V0 does not have a native JIRA connector in its Vercel Marketplace (which covers Neon, Supabase, Upstash, and Stripe). JIRA integrates via the standard Next.js API Route pattern — you install no special packages beyond a fetch wrapper, set credentials as Vercel environment variables, and call JIRA's REST API from your API routes.
Can I connect to Jira Server (self-hosted) as well as Jira Cloud?
Yes. Both Jira Cloud and Jira Server support REST API v3, but with different base URLs. For Jira Cloud, the base URL is https://your-company.atlassian.net. For Jira Server, it's your organization's self-hosted domain (e.g., https://jira.yourcompany.com). Authentication also differs slightly — Jira Server may use HTTP Basic Auth with your regular username and password, while Jira Cloud requires the email + API token pattern. Set JIRA_BASE_URL accordingly in Vercel.
What is JQL and do I need to learn it to use the JIRA API?
JQL (JIRA Query Language) is JIRA's search syntax for filtering issues. You only need a few basic patterns: 'project = KEY' to filter by project, 'sprint in openSprints()' for the current sprint, 'issuetype = Bug' for bug filtering, and 'assignee = currentUser()' for personal views. You can build these strings dynamically in your API routes and pass them as URL-encoded query parameters. JIRA's own issue search UI shows the JQL behind any filtered view — click 'Advanced' in JIRA's search to see the JQL for any filter you build there.
How do I build drag-and-drop status transitions on the sprint board?
Drag-and-drop requires calling JIRA's transition endpoint when an issue card is dropped in a new column. First, fetch available transitions for the issue with GET /rest/api/3/issue/{id}/transitions. Each transition has an ID and name (e.g., 'Start Progress', 'Done'). When a card is dropped, match the target column name to the right transition ID and POST to /rest/api/3/issue/{id}/transitions with the transition ID. Transition IDs are workflow-specific, so fetch them dynamically rather than hardcoding them.
Can I use JIRA webhooks in my V0 app to get real-time issue updates?
Yes — JIRA Cloud supports webhooks that POST to your API route when issues are created, updated, or deleted. Register a webhook in JIRA System Settings → System → WebHooks pointing to your Vercel deployment URL (e.g., https://your-app.vercel.app/api/jira/webhook). Your webhook handler receives a JSON payload with issue details. Note that JIRA webhooks require your app to be deployed to a public URL — they cannot call localhost, so you need a deployed Vercel URL for testing.
Is the JIRA API free to use?
JIRA Cloud's REST API is included with all plans, including the free tier (up to 10 users). You don't pay extra for API access. JIRA Server/Data Center requires a license but also includes API access. The main limitation is rate limiting — JIRA Cloud has undocumented rate limits that typically allow several hundred requests per minute per user, well above what a dashboard will need.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation