Skip to main content
This guide teaches you how to create a feedback collection system using Solana Blinks with message signing. Unlike traditional on-chain transactions that cost SOL, message signing is completely free, making it perfect for collecting user feedback, surveys, or any data that doesn’t need to be stored on-chain.
Level: IntermediateTime Required: 30-45 minutesGitHub Repository: dialectlabs/feedback-blink

Watch the Tutorial

▶️ Watch: Building a Feedback Blink with Message Signing - Follow along as we build this feedback blink from scratch, set up a database, and implement message signing.

What You’ll Learn

In this tutorial, you will:
  • Build a message signing blink (no transaction fees!)
  • Set up a PostgreSQL database with Neon
  • Use Prisma ORM for database operations
  • Implement action chaining for multi-step flows
  • Create text input parameters for user feedback
  • Handle CORS and proper headers for blinks
  • Deploy and test your feedback collection system

Prerequisites

Before starting this tutorial, ensure you have:
  • Code Editor of your choice (recommended Cursor or Visual Studio Code)
  • Node.js 18.x.x or above installed
  • Basic understanding of TypeScript and Next.js
  • A Neon account for the database (free tier available)
  • A Solana wallet for testing (no SOL required - message signing is free!)
  • Basic familiarity with REST APIs and databases

Project Setup

Let’s start by creating a new project using the Dialect Solana Starter template. This project includes NextJS as well as all the necessary dependencies for building apps on the Solana Blockchain, including the Solana Actions and Blinks SDK.

Step 1: Initialize the Project

Use the Dialect scaffold to quickly set up a new blink project:
npx create-blinks-app
When prompted, configure your project:
✓ Select a blockchain: Solana
✓ Project name: feedback-blink
Navigate to your project directory:
cd feedback-blink

Step 2: Install Additional Dependencies

We need Prisma for database management:
  • npm
  • yarn
  • pnpm
npm install prisma @prisma/client

Step 3: Initialize Prisma

Set up Prisma with PostgreSQL:
npx prisma init
This creates:
  • prisma/schema.prisma - Your database schema file
  • .env - Environment variables file (add to .gitignore!)

Step 4: Add the Feedback Image

Every blink needs an icon image. Add your feedback image to the public folder:
  1. Create or find a feedback-related image (recommended: 1200x630px for optimal display)
  2. Save it as feedback.jpg in the public/ folder
  3. The image will be available at /feedback.jpg in your application

Database Setup with Neon

Let’s set up a PostgreSQL database using Neon’s serverless platform.

Step 1: Create a Neon Database

  1. Visit Neon.tech and sign up for a free account
  2. Click “Create a project”
  3. Name your project: feedback-blink
  4. Select your region (choose one close to your users)
  5. Click “Create project”

Step 2: Get Your Database Connection String

  1. In the Neon dashboard, click “Connection Details”
  2. Copy the connection string (it looks like: postgresql://username:password@host/database)
  3. Keep this tab open - you’ll need this string in the next step

Step 3: Configure Environment Variables

Update your .env file with the database connection:
# Database connection string from Neon
DATABASE_URL="postgresql://username:password@host/database?sslmode=require"

# Optional: Solana RPC endpoint (defaults to mainnet)
NEXT_PUBLIC_SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
Never commit your .env file to version control! Ensure it’s in your .gitignore file.

Step 4: Define the Database Schema

Update prisma/schema.prisma with our feedback model:
prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Feedback {
  id        Int      @id @default(autoincrement())
  wallet    String   @db.VarChar(255)
  feedback  String   @db.Text
  createdAt DateTime @default(now())
  
  // Index on wallet for faster queries
  @@index([wallet])
}
This schema creates a table with:
  • id - Auto-incrementing primary key
  • wallet - The user’s Solana wallet address
  • feedback - The actual feedback text (unlimited length)
  • createdAt - Timestamp of when feedback was submitted
  • Index on wallet for efficient queries by user

Step 5: Create the Database Tables

Push the schema to your Neon database:
npx prisma migrate dev --name init-feedback
This command:
  1. Creates the SQL migration files
  2. Applies the migration to your database
  3. Generates the Prisma client

Step 6: Generate the Prisma Client

Generate the TypeScript client:
npx prisma generate

Step 7: Create the Prisma Client Singleton

Next.js in development mode can create multiple instances of the Prisma client. Create a singleton to prevent this:
src/lib/prisma.ts
import { PrismaClient } from "../generated/prisma";

const globalForPrisma = global as unknown as { prisma: PrismaClient };

export const prisma =
    globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
This singleton pattern prevents “too many connections” errors during development with hot reloading.
Now let’s create the actual blink endpoints.

Step 1: Create the Folder Structure

Create the following folder structure for your API routes:
src/
└── app/
    ├── api/
    │   └── actions/
    │       └── feedback/
    │           ├── route.ts           # Main feedback endpoint
    │           └── complete/
    │               └── route.ts       # Completion endpoint
    └── actions.json/
        └── route.ts                   # Actions discovery file

Step 2: Create the Actions Discovery File

The actions.json endpoint tells blink clients what actions are available:
src/app/actions.json/route.ts
import { ACTIONS_CORS_HEADERS, ActionsJson } from "@solana/actions";

export const GET = async () => {
  const payload: ActionsJson = {
    rules: [
      // Map all root level routes to an action
      {
        pathPattern: "/*",
        apiPath: "/api/actions/*",
      },
      // Fallback for nested routes
      {
        pathPattern: "/api/actions/**",
        apiPath: "/api/actions/**",
      },
    ],
  };

  return Response.json(payload, {
    headers: ACTIONS_CORS_HEADERS,
  });
};

// Required for CORS preflight requests
export const OPTIONS = GET;

Step 3: Create the Main Feedback Endpoint

This is where the magic happens. Let’s build the main feedback collection endpoint:
src/app/api/actions/feedback/route.ts
import { 
  ActionGetResponse, 
  ActionPostRequest, 
  ActionPostResponse, 
  ACTIONS_CORS_HEADERS, 
  BLOCKCHAIN_IDS 
} from "@solana/actions";
import { prisma } from "@/lib/prisma";

// Use CAIP-2 format for Solana Mainnet
const blockchain = BLOCKCHAIN_IDS.mainnet;

// Headers configuration with blockchain ID and version
const headers = {
  ...ACTIONS_CORS_HEADERS,
  'X-Blockchain-Ids': blockchain,
  'X-Action-Version': '2.4'
};

// OPTIONS handler for CORS preflight requests
export const OPTIONS = async () => {
  return new Response(null, { headers });
}

// GET handler - Returns the blink UI configuration
export const GET = async (req: Request) => {
  const payload: ActionGetResponse = {
    type: 'action',
    title: 'Share Your Feedback',
    description: 'Help us improve Blinks! Tell us what features you need or what could be better.',
    icon: new URL('/feedback.jpg', req.url).toString(),
    label: 'Send Feedback',
    links: {
      actions: [
        {
          // Using 'message' type for message signing (not 'transaction')
          type: 'message',
          href: '/api/actions/feedback?feedback={feedback}',
          label: 'Send Feedback',
          parameters: [
            {
              name: 'feedback',
              type: 'text',  // Use 'text' for single line, 'textarea' for multi-line
              required: true,
              label: "What features are missing or what could be improved?"
            }
          ]
        }
      ]
    }
  };

  return new Response(JSON.stringify(payload), { headers });
};

// POST handler - Processes and stores the feedback
export const POST = async (req: Request) => {
  try {
    // Extract the user's wallet address from the request
    const request: ActionPostRequest = await req.json();
    const { account } = request;

    // Get feedback from URL parameters
    const url = new URL(req.url);
    const feedback = url.searchParams.get("feedback");

    // Validate feedback
    if (!feedback || feedback.trim().length === 0) {
      return new Response(
        JSON.stringify({ error: "Feedback cannot be empty" }), 
        { status: 400, headers }
      );
    }

    // Save feedback to database
    const savedFeedback = await prisma.feedback.create({
      data: {
        wallet: account,
        feedback: feedback.trim()
      }
    });

    console.log(`Feedback saved: ID ${savedFeedback.id} from ${account}`);

    // Return success with chaining to completion step
    const payload: ActionPostResponse = {
      type: "message",
      message: "Thank you for your feedback! 🙏",
      links: {
        next: {
          type: "post",
          href: "/api/actions/feedback/complete"
        }
      }
    }

    return new Response(JSON.stringify(payload), { headers });
    
  } catch (error) {
    console.error("Error saving feedback:", error);
    return new Response(
      JSON.stringify({ error: "Failed to save feedback" }), 
      { status: 500, headers }
    );
  }
};

Step 4: Create the Completion Endpoint

The completion endpoint provides a final confirmation to the user:
src/app/api/actions/feedback/complete/route.ts
import { ACTIONS_CORS_HEADERS, BLOCKCHAIN_IDS, ActionGetResponse } from "@solana/actions";

// Use the same blockchain ID as the main endpoint
const blockchain = BLOCKCHAIN_IDS.mainnet;

const headers = {
  ...ACTIONS_CORS_HEADERS,
  'X-Blockchain-Ids': blockchain,
  'X-Action-Version': '2.4'
};

// OPTIONS handler for CORS
export const OPTIONS = async () => {
  return new Response(null, { headers });
}

// POST handler - Final step in the feedback flow
export const POST = async (req: Request) => {
  const payload: ActionGetResponse = {
    type: "action",
    icon: new URL('/feedback.jpg', req.url).toString(),
    title: "Feedback Received!",
    description: "Your feedback has been successfully recorded. Thank you for helping us improve!",
    label: "Done",
    links: {
      // Empty actions array indicates the flow is complete
      actions: []
    }
  };

  return new Response(JSON.stringify(payload), { headers });
}

Understanding Key Concepts

Message Signing vs Transactions

This blink uses message signing instead of blockchain transactions:
FeatureMessage SigningTransaction
CostFreeRequires SOL for fees
SpeedInstantRequires confirmation
StorageOff-chain (database)On-chain
Use CaseFeedback, authenticationPayments, NFTs
Production-Ready Message SigningThis guide demonstrates basic message signing for tutorial purposes. For production applications requiring enhanced security, anti-replay protection, and proper authentication flows, see our Message Signing documentation in the advanced section.

Action Chaining

The feedback flow uses action chaining to create a multi-step user experience:
  1. Initial Action - User enters feedback
  2. Message Signing - User signs the message with their wallet
  3. Completion - Final confirmation screen
This is achieved using the links.next field in the response:
links: {
  next: {
    type: "post",
    href: "/api/actions/feedback/complete"
  }
}

CORS Headers

Blinks require specific CORS headers to work across different domains:
  • Access-Control-Allow-Origin: * - Allows any domain
  • X-Blockchain-Ids - Identifies supported blockchains
  • X-Action-Version - Specifies the Actions spec version

Step 1: Start the Development Server

npm run dev
Your application will be available at http://localhost:3000

Step 2: Test on the Landing Page

The scaffold includes a built-in Blinks client on the landing page. Update src/app/page.tsx to point at your new feedback endpoint. Here are minimal snippets — keep the rest of your page code as-is.
src/app/page.tsx
// ... imports and other code from above

const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';

const blinkApiUrl = `${baseUrl}/api/actions/feedback`;

const { adapter } = useBlinkSolanaWalletAdapter("https://api.mainnet-beta.solana.com");
const { blink, isLoading } = useBlink({ url: blinkApiUrl });

// ... rest of the code
Then visit http://localhost:3000 and interact with the blink directly on the page. Note: You can still use dial.to as an alternative by pasting http://localhost:3000/api/actions/feedback.

Step 3: Verify Database Entries

Check your database using Prisma Studio:
npx prisma studio
This opens a web interface where you can:
  • View all feedback entries
  • See wallet addresses and timestamps
  • Export data if needed

Step 4: Test Error Handling

Try these scenarios:
  • Submit empty feedback (should show error)
  • Submit very long feedback (should work)
  • Submit multiple times from the same wallet (should all be saved)

Conclusion

Congratulations! You’ve built a complete feedback collection system using Solana Blinks with message signing. You’ve learned how to: The feedback blink pattern is incredibly versatile. You can adapt it for:
  • Customer surveys
  • Bug reports
  • Contact forms
  • User testimonials
  • Newsletter signups
  • Any data collection that doesn’t require on-chain storage
Happy cooking! 🧑‍🍳
I