Quick set up for decentralized frontends
Centralized infrastructure still controls most “decentralized” apps at the frontend layer. Your smart contracts can be censorship-resistant, but if your UI lives on a single Web2 host, users can lose access overnight.
This post is a quick guide to ship your frontend to IPFS automatically on every main push.
What Is IPFS (And Why It Matters)
IPFS (InterPlanetary File System) is a content-addressed network. Instead of fetching files from a server location, the users fetch content by hash (CID). That means:
- Content is identified by what it is, not where it is hosted.
- Any node pinning the same CID can serve that content.
- You get stronger integrity guarantees, because the hash must match.
For decentralized products, this matters because hosting is often the weakest link.
Why Decentralized Frontends Are Critical
There are many moments where frontend decentralization is not optional:
- Provider outage concentration: In Nov 2025, a major Cloudflare incident reportedly impacted a large share of internet traffic and disrupted access to multiple dApps.
- Cloud DNS/control-plane failures: In Oct 2025, an AWS outage tied to DNS resolution issues disrupted global apps.
- Regulatory frontend takedowns: OFAC’s 2022 Tornado Cash sanctions targeted its web/domain access layer; even before 2025 delisting, contract code existing onchain did not guarantee practical user access.
If users cannot access the interface, protocol-level decentralization is not enough in practice.
Important Tradeoff Up Front: Pinata Is Step 1, Not the End State
In this guide i propose Pinata as a bootstrap layer because it is simple and fast to integrate into CI.
But keep in mind that single-provider pinning creates a single failure domain for availability, policy decisions, and billing.
If that provider degrades, delists content, or shuts down, your users will lose the interface.
Treat this guide as phase one: baseline automation. Phase two is redundancy, where each release is pinned across multiple independent providers and/or your own nodes.
Repository Prerequisites
Build output
Your build must produce a static directory:
- Next.js: usually
./out(output: 'export'is required) - Vite/React: usually
./dist
In this guide we assume ./out.
Create the GitHub Actions Workflow
Create this file in your repository:
.github/workflows/release.yml
Paste:
name: Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate release tag
id: release-tag
run: echo "tag=release-$(date -u +"%Y-%m-%d_%H-%M")" >> "$GITHUB_OUTPUT"
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "21.4"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build static export
run: pnpm build
- name: Pin to IPFS (Pinata)
id: upload
uses: aquiladev/ipfs-action@v0.3.1
with:
path: ./out
service: pinata
pinataKey: $
pinataSecret: $
pinName: "Release $"
- name: Convert CIDv0 to CIDv1
id: convert_cid
uses: uniswap/convert-cidv0-cidv1@v1.0.0
with:
cidv0: $
- name: Tag version
id: tag
uses: uniswap/github-tag-action@7bddacd4864a0f5671e836721db60174d8a9c399
with:
github_token: $
custom_tag: $
tag_prefix: ""
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: $
name: Production Release
body: |
IPFS hash of the deployment:
- CIDv0: `$`
- CIDv1: `$`
You can also access the interface from an IPFS gateway.
IPFS gateways:
- https://$.ipfs.dweb.link/
- ipfs://$/
draft: false
prerelease: false
Configure GitHub Secrets
Go to: Settings -> Secrets and variables -> Actions -> New repository secret
Add the following keys (from Pinata Developers):
| Secret Name | Description |
|---|---|
PINATA_API_KEY |
Your Pinata API Key |
PINATA_SECRET_API_KEY |
Your Pinata Secret API Key |
Framework Configuration
IPFS serves static files. If your app needs a server at runtime, it will break.
Option A: Next.js
- Update
next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export", // Required for IPFS static hosting
trailingSlash: true, // Avoid common gateway path 404s
images: { unoptimized: true }, // Next image optimization needs a server
};
export default nextConfig;
- Keep your
package.jsonbuild script asnext build. - The workflow uses
./out, which is correct for Next static export.
Option B: Vite (React/Vue)
- Change workflow upload path from
./outto./dist:
path: ./dist
- Configure
vite.config.jswith a relative base:
import { defineConfig } from "vite";
export default defineConfig({
base: "./", // Important for IPFS path resolution
});
What to Improve Next
Think of this setup as a good starting point. For stronger protection and real-world reliability, here’s what you might want to do next:
- Multi-provider pinning (Pinata + another pinning service)
- Self-hosted IPFS pinning node (if possible)
- Monitoring that checks gateway availability per CID
- Optional ENS/IPNS naming for better UX