DEV Community: Prosper Otemuyiwa The latest articles on DEV Community by Prosper Otemuyiwa (@unicodeveloper). https://dev.to/unicodeveloper https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F304853%2Fb1f707ad-5975-4aca-a2bd-db854a60dda5.jpg DEV Community: Prosper Otemuyiwa https://dev.to/unicodeveloper en Building Multi-Agent Research Systems using Vercel AI SDK Prosper Otemuyiwa Fri, 13 Mar 2026 13:26:08 +0000 https://dev.to/valyuai/building-multi-agent-research-systems-using-vercel-ai-sdk-2lae https://dev.to/valyuai/building-multi-agent-research-systems-using-vercel-ai-sdk-2lae <p>There are many ways to build apps and systems. In today’s AI-native world, the possibilities are endless.</p> <p>Now imagine you’re tasked with building a multi-agent research system, with one key requirement: don’t over-engineer it.</p> <p><strong>KISS — Keep It Simple, Stupid.</strong></p> <p>This is the scenario given to you:</p> <p>"A friend asked me to pull together everything on Eli Lilly's Q4 results, any ongoing GLP-1 trials they've filed recently, and how the financial press was covering it. Build a multi-agent research system that handles that so you can go accomplish 10 other things while the system gets you the result."</p> <p>Now, let’s break this down:</p> <ul> <li>Pull together everything on Eli Lilly’s Q4 results</li> <li>Identify any GLP-1 trials they’ve filed recently</li> <li>See how the financial press has been covering it</li> </ul> <p>When broken down like this, it’s simply three queries across three completely different domains: SEC filings, clinical trials, and live news.</p> <p>Three different tools, three different contexts and then stitching it all together manually at the end. It’s similar to the <strong><em>"15-tab" problem.</em></strong></p> <p>And it gets worse when you’re building an AI app. You often end up maintaining a research pipeline that’s just a collection of disconnected scripts, held together by copy-paste.</p> <h2> The Architecture </h2> <p>We have three different contexts, so three domains. We'll use the <strong>Vercel AI SDK</strong> to handle all three domains in parallel.</p> <p>One query, three specialist agents running simultaneously, and a single synthesized response. With the <a href="proxy.php?url=https://www.npmjs.com/package/@valyu/ai-sdk" rel="noopener noreferrer">@valyu/ai-sdk</a> package, plugging in domain-specific data sources takes just one import—no manual tool definitions required.</p> <p>This is the architecture: the <strong>Orchestrator–Worker</strong> pattern.</p> <p>One orchestrator agent understands the query, dispatches the right specialists in parallel, and synthesizes the results. Each specialist has its own tool suite and domain expertise</p> <p><a href="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vuvbowzhuklmpc05tgv.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vuvbowzhuklmpc05tgv.png" alt="Orchestrator Worker pattern" width="800" height="615"></a></p> <p>The tools come from <a href="proxy.php?url=https://www.npmjs.com/package/@valyu/ai-sdk" rel="noopener noreferrer">@valyu/ai-sdk</a>, a package that provides ready-made <a href="proxy.php?url=https://ai-sdk.valyu.ai/" rel="noopener noreferrer">Vercel AI SDK tools</a> backed by Valyu's search API. No manual <code>tool()</code> definitions, no Zod schemas for parameters, no custom execute functions. </p> <p>Import the tool, drop it into your <code>tools</code> object, done.</p> <h3> Why Parallel Dispatch Matters </h3> <p>Sequential agents are the wrong default for research workloads. Each specialist takes 4-6 seconds. </p> <p>Three in sequence is 15+ seconds. Three specialists running simultaneously gets you results in the time it takes the slowest one to finish.</p> <p><a href="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l7i7sevq1e0nrgfesm4.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l7i7sevq1e0nrgfesm4.png" alt="Sequential vs Parallel Execution" width="800" height="527"></a></p> <h2> Data Flow: From Query to Report </h2> <p>For a query like "What's Eli Lilly's financial position and do their GLP-1 trials support the revenue projections?":</p> <p><a href="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnf769oyu7fba98fpgdp9.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnf769oyu7fba98fpgdp9.png" alt="Data flow - from query to report" width="800" height="446"></a></p> <h2> Setup </h2> <p>Install Nextjs and then add the following packages...<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>pnpm add ai @ai-sdk/anthropic @valyu/ai-sdk @ai-sdk/react zod valyu-js </code></pre> </div> <p>Create <code>.env.local</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>ANTHROPIC_API_KEY=your_key_here VALYU_API_KEY=your_key_here </code></pre> </div> <p>Grab your <a href="proxy.php?url=https://platform.claude.com/settings/keys" rel="noopener noreferrer">Anthropic</a> and <a href="proxy.php?url=https://platform.valyu.ai" rel="noopener noreferrer">Valyu API</a> keys.</p> <p>Both keys are read from environment automatically. <code>@valyu/ai-sdk</code> picks up <code>VALYU_API_KEY</code> without any explicit configuration.</p> <p>Project structure:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>research-nexus/ ├── src/ │ ├── agents/ │ │ ├── financial-analyst.ts # Financial analyst │ │ ├── scientist.ts # Scientist │ │ ├── journalist.ts # Journalist │ │ └── orchestrator.ts # Query router + synthesizer │ └── app/ │ └── api/ │ └── chat/ │ └── route.ts # Next.js API route └── package.json </code></pre> </div> <p>No <code>lib/</code> or <code>tools/</code> directories. The tools come pre-built.</p> <h2> The Specialist Agents </h2> <p>Each specialist is a <code>ToolLoopAgent</code> with domain-specific tools from @valyu/ai-sdk. The agent loop lets the model chain multiple tool calls before returning. Searching SEC filings, then cross-referencing earnings, then adding macro context, all within a single agent invocation.</p> <h3> Financial Analyst </h3> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ToolLoopAgent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">anthropic</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@ai-sdk/anthropic</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">secSearch</span><span class="p">,</span> <span class="nx">financeSearch</span><span class="p">,</span> <span class="nx">economicsSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/ai-sdk</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">financialAnalystAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ToolLoopAgent</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">anthropic</span><span class="p">(</span><span class="dl">"</span><span class="s2">claude-haiku-4-5-20251001</span><span class="dl">"</span><span class="p">),</span> <span class="na">instructions</span><span class="p">:</span> <span class="s2">`You are a senior financial analyst specializing in SEC filings, market data, and economic research. Your capabilities: - Search and analyze SEC filings (10-K, 10-Q, 8-K, proxy statements) - Look up financial data including stock prices, earnings, income statements - Research economic indicators and macro data When responding: - Always cite the specific filing type and date - Present financial figures clearly with proper formatting - Highlight key risks, trends, and material changes - Compare metrics across periods when relevant - Be precise about numbers — never approximate when exact data is available`</span><span class="p">,</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">secSearch</span><span class="p">:</span> <span class="nf">secSearch</span><span class="p">({</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">short</span><span class="dl">"</span> <span class="p">}),</span> <span class="na">financeSearch</span><span class="p">:</span> <span class="nf">financeSearch</span><span class="p">({</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">short</span><span class="dl">"</span> <span class="p">}),</span> <span class="na">economicsSearch</span><span class="p">:</span> <span class="nf">economicsSearch</span><span class="p">({</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">short</span><span class="dl">"</span> <span class="p">}),</span> <span class="p">},</span> <span class="p">});</span> </code></pre> </div> <p><em>financial-analyst.ts</em></p> <h3> Scientist </h3> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ToolLoopAgent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">anthropic</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@ai-sdk/anthropic</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">bioSearch</span><span class="p">,</span> <span class="nx">paperSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/ai-sdk</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">medicalResearcherAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ToolLoopAgent</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">anthropic</span><span class="p">(</span><span class="dl">"</span><span class="s2">claude-haiku-4-5-20251001</span><span class="dl">"</span><span class="p">),</span> <span class="na">instructions</span><span class="p">:</span> <span class="s2">`You are a medical and life sciences research specialist with expertise in clinical trials, drug discovery, and biomedical literature. Your capabilities: - Search clinical trial databases for trial status, results, and endpoints - Look up FDA drug labels, approvals, and safety information - Research biomedical literature from PubMed, bioRxiv, and medRxiv - Analyze academic papers on drugs, therapies, and medical devices When responding: - Always cite trial IDs (NCT numbers), DOIs, or publication references - Clearly distinguish between preliminary and peer-reviewed findings - Note the phase of clinical trials and their primary endpoints - Flag any safety concerns or adverse events mentioned in the data - Use proper medical terminology but explain it when needed`</span><span class="p">,</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">bioSearch</span><span class="p">:</span> <span class="nf">bioSearch</span><span class="p">({</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">short</span><span class="dl">"</span> <span class="p">}),</span> <span class="na">paperSearch</span><span class="p">:</span> <span class="nf">paperSearch</span><span class="p">({</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">short</span><span class="dl">"</span> <span class="p">}),</span> <span class="p">},</span> <span class="p">});</span> </code></pre> </div> <p><em>scientist.ts</em></p> <h3> Journalist </h3> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ToolLoopAgent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">anthropic</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@ai-sdk/anthropic</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">webSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/ai-sdk</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">journalistAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ToolLoopAgent</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">anthropic</span><span class="p">(</span><span class="dl">"</span><span class="s2">claude-haiku-4-5-20251001</span><span class="dl">"</span><span class="p">),</span> <span class="na">instructions</span><span class="p">:</span> <span class="s2">`You are an investigative journalist and news analyst with access to real-time web sources. Your capabilities: - Search the web for breaking news and current events - Find and cross-reference multiple news sources on a topic - Track developing stories and provide timeline context - Research background on people, organizations, and events When responding: - Always attribute information to specific sources - Present multiple perspectives when covering controversial topics - Distinguish between confirmed facts and unverified reports - Provide publication dates so readers know how current the information is - Summarize key points clearly, then provide supporting details`</span><span class="p">,</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">webSearch</span><span class="p">:</span> <span class="nf">webSearch</span><span class="p">({</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">short</span><span class="dl">"</span> <span class="p">}),</span> <span class="p">},</span> <span class="p">});</span> </code></pre> </div> <p><em>journalist.ts</em></p> <p>Three agents, three imports from <code>@valyu/ai-sdk</code>, zero custom tool definitions.</p> <h2> The Orchestrator </h2> <p>The orchestrator handles query classification, parallel dispatch, and synthesis.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ToolLoopAgent</span><span class="p">,</span> <span class="nx">tool</span><span class="p">,</span> <span class="nx">stepCountIs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">anthropic</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@ai-sdk/anthropic</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">z</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">zod</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">financialAnalystAgent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./financial-analyst</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">scientistAgent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./scientist</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">journalistAgent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./journalist</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">financialAnalystTool</span> <span class="o">=</span> <span class="nf">tool</span><span class="p">({</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Delegate to the Financial Analyst agent for SEC filings, stock data, earnings reports, economic indicators, and financial analysis.</span><span class="dl">"</span><span class="p">,</span> <span class="na">inputSchema</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">object</span><span class="p">({</span> <span class="na">task</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">string</span><span class="p">().</span><span class="nf">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">The financial research task to complete</span><span class="dl">"</span><span class="p">),</span> <span class="p">}),</span> <span class="na">execute</span><span class="p">:</span> <span class="k">async </span><span class="p">({</span> <span class="nx">task</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">abortSignal</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">financialAnalystAgent</span><span class="p">.</span><span class="nf">generate</span><span class="p">({</span> <span class="na">prompt</span><span class="p">:</span> <span class="nx">task</span><span class="p">,</span> <span class="nx">abortSignal</span><span class="p">,</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">result</span><span class="p">.</span><span class="nx">text</span><span class="p">;</span> <span class="p">},</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">scientistTool</span> <span class="o">=</span> <span class="nf">tool</span><span class="p">({</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Delegate to the Scientist agent for clinical trials, drug information, FDA data, biomedical papers, and life sciences research.</span><span class="dl">"</span><span class="p">,</span> <span class="na">inputSchema</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">object</span><span class="p">({</span> <span class="na">task</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">string</span><span class="p">().</span><span class="nf">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">The medical/life sciences research task to complete</span><span class="dl">"</span><span class="p">),</span> <span class="p">}),</span> <span class="na">execute</span><span class="p">:</span> <span class="k">async </span><span class="p">({</span> <span class="nx">task</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">abortSignal</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">scientistAgent</span><span class="p">.</span><span class="nf">generate</span><span class="p">({</span> <span class="na">prompt</span><span class="p">:</span> <span class="nx">task</span><span class="p">,</span> <span class="nx">abortSignal</span><span class="p">,</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">result</span><span class="p">.</span><span class="nx">text</span><span class="p">;</span> <span class="p">},</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">journalistTool</span> <span class="o">=</span> <span class="nf">tool</span><span class="p">({</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Delegate to the Journalist agent for real-time news, current events, breaking stories, and web-based research on any topic.</span><span class="dl">"</span><span class="p">,</span> <span class="na">inputSchema</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">object</span><span class="p">({</span> <span class="na">task</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">string</span><span class="p">().</span><span class="nf">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">The news/research task to complete</span><span class="dl">"</span><span class="p">),</span> <span class="p">}),</span> <span class="na">execute</span><span class="p">:</span> <span class="k">async </span><span class="p">({</span> <span class="nx">task</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">abortSignal</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">journalistAgent</span><span class="p">.</span><span class="nf">generate</span><span class="p">({</span> <span class="na">prompt</span><span class="p">:</span> <span class="nx">task</span><span class="p">,</span> <span class="nx">abortSignal</span><span class="p">,</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">result</span><span class="p">.</span><span class="nx">text</span><span class="p">;</span> <span class="p">},</span> <span class="p">});</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">orchestratorAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ToolLoopAgent</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">anthropic</span><span class="p">(</span><span class="dl">"</span><span class="s2">claude-haiku-4-5-20251001</span><span class="dl">"</span><span class="p">),</span> <span class="na">instructions</span><span class="p">:</span> <span class="s2">`You are a research orchestrator that routes queries to specialized agents. You have three specialist agents available: 1. **Financial Analyst** — SEC filings, stock data, earnings, financial statements, economic data 2. **Scientist** — Clinical trials, drug discovery, FDA data, biomedical papers 3. **Journalist** — Real-time news, current events, web research Your job: - Analyze the user's query and delegate to the right specialist(s) - For questions that span multiple domains, call multiple agents - Synthesize results from multiple agents into a coherent response - If a query doesn't fit any specialist, answer it yourself - Always be clear about which sources informed your response`</span><span class="p">,</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">financialAnalyst</span><span class="p">:</span> <span class="nx">financialAnalystTool</span><span class="p">,</span> <span class="na">scientist</span><span class="p">:</span> <span class="nx">scientistTool</span><span class="p">,</span> <span class="na">journalist</span><span class="p">:</span> <span class="nx">journalistTool</span><span class="p">,</span> <span class="p">},</span> <span class="na">stopWhen</span><span class="p">:</span> <span class="nf">stepCountIs</span><span class="p">(</span><span class="mi">10</span><span class="p">),</span> <span class="p">});</span> </code></pre> </div> <p>The orchestrator agent acts as a smart router. It receives the user's query, analyzes what domains it touches, and delegates work to the right specialist(s). </p> <p>It doesn't do the research itself. Instead, it calls one or more sub-agents as tools: </p> <ul> <li>Financial Analyst for SEC/market data, </li> <li>Scientist for clinical trials and biomedical literature,</li> <li>Journalist for real-time news. </li> </ul> <p>For cross-domain questions like "How does Eli Lilly's GLP-1 pipeline affect their stock outlook?", it calls multiple specialists in sequence.</p> <p><strong>Sub-Agents</strong> as <strong>Tools</strong>. Each specialist is a <strong>ToolLoopAgent</strong> wrapped in a tool() call, which makes it callable by the orchestrator just like any other function. </p> <p>When invoked, the sub-agent runs its own independent loop, calling Valyu search tools (like <strong>secSearch</strong> or <strong>bioSearch</strong>), reading the results, and synthesizing a response. </p> <p>The sub-agent's final text is returned to the orchestrator as the tool's output.</p> <p><strong>Loop Control</strong>. The <strong>stopWhen: stepCountIs(10)</strong> on the orchestrator caps it at 10 loop iterations to prevent runaway execution. The sub-agents use the default limit of 20 steps. Within those bounds, each agent is free to make multiple tool calls. For example, the Financial Analyst might search SEC filings first, then cross-reference with earnings data, all within a single invocation before returning its findings.</p> <h2> The API Route </h2> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// app/api/chat/route.ts</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">createAgentUIStreamResponse</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">orchestratorAgent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/agents/orchestrator</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">POST</span><span class="p">(</span><span class="nx">request</span><span class="p">:</span> <span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">messages</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">request</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span> <span class="k">return</span> <span class="nf">createAgentUIStreamResponse</span><span class="p">({</span> <span class="na">agent</span><span class="p">:</span> <span class="nx">orchestratorAgent</span><span class="p">,</span> <span class="na">uiMessages</span><span class="p">:</span> <span class="nx">messages</span><span class="p">,</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>For the sake of keeping this post short, here’s the repo: <a href="proxy.php?url=https://github.com/unicodeveloper/multi-agent-research-sys" rel="noopener noreferrer">multi-agent-research-sys</a>.</p> <p>You’ll find all the UI pages there. Clone the project and run it locally to explore the full setup.</p> <h2> Running it </h2> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>pnpm dev </code></pre> </div> <p><strong>Output:</strong></p> <p><em>User entered query</em><br> <a href="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxw53ymd9gjb5rmu1nns7.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxw53ymd9gjb5rmu1nns7.png" alt="User enters query..." width="800" height="520"></a></p> <p><em>Result shows up</em><br> <a href="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqd4ip40uzrgpmtckqgnc.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqd4ip40uzrgpmtckqgnc.png" alt="This result shows up" width="800" height="453"></a></p> <p><a href="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomtg4lzqgzfhd5re3alo.gif" class="article-body-image-wrapper"><img src="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomtg4lzqgzfhd5re3alo.gif" alt="Result" width="200" height="120"></a></p> <h2> What <code>@valyu/ai-sdk</code> Provides </h2> <p>The package ships ten tools that cover the major research verticals:</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Tool</th> <th>Data Sources</th> </tr> </thead> <tbody> <tr> <td><code>secSearch()</code></td> <td>SEC 10-K, 10-Q, 8-K filings, EDGAR full-text</td> </tr> <tr> <td><code>financeSearch()</code></td> <td>Stocks, earnings, balance sheets, insider trades, dividends</td> </tr> <tr> <td><code>economicsSearch()</code></td> <td>FRED, BLS, World Bank, US government spending</td> </tr> <tr> <td><code>bioSearch()</code></td> <td>ClinicalTrials.gov, DrugBank, ChEMBL, FDA labels, Open Targets</td> </tr> <tr> <td><code>paperSearch()</code></td> <td>PubMed, arXiv, bioRxiv, medRxiv, academic publishers</td> </tr> <tr> <td><code>webSearch()</code></td> <td>Real-time web, news, general content</td> </tr> <tr> <td><code>patentSearch()</code></td> <td>USPTO, global patent databases</td> </tr> <tr> <td><code>companyResearch()</code></td> <td>Comprehensive company intelligence</td> </tr> <tr> <td><code>datasources()</code></td> <td>List available data sources</td> </tr> <tr> <td><code>datasourcesCategories()</code></td> <td>List available categories</td> </tr> </tbody> </table></div> <p>Each tool accepts optional configuration: <code>maxNumResults</code>, <code>relevanceThreshold</code>, <code>includedSources</code> for source-level filtering. All read <code>VALYU_API_KEY</code> from the environment by default.</p> <p>The difference from web only search matters most for the financial and biomedical agents. </p> <p>A web search for "Eli Lilly 10-K 2024 risk factors" returns SEO articles about the filing. <code>secSearch()</code> returns the actual filing text. <br> A web search for "tirzepatide Phase 3 results" returns health news coverage. <code>bioSearch()</code> returns the ClinicalTrials.gov entries and DrugBank compound data. <br> That's primary source access vs secondary commentary. A meaningful difference for research quality.</p> <h2> Extending the System </h2> <p>Adding a fourth specialist (patents, legal, government) requires:</p> <ol> <li>One agent file importing the relevant <code>@valyu/ai-sdk</code> tools</li> <li>Adding the new agent to the orchestrator</li> </ol> <p>Ten specialists in parallel takes the same wall-clock time as three.</p> <p>The full code is on <a href="proxy.php?url=https://github.com/unicodeveloper/multi-agent-research-sys" rel="noopener noreferrer">GitHub</a>. Clone, run and explore!</p> <p>Get a Valyu API key at <a href="proxy.php?url=https://platform.valyu.ai" rel="noopener noreferrer">platform.valyu.ai</a>. $10 free credit, no credit card required.</p> ai agents aisdk valyu Perplexity Sonar Alternatives for Developers (2026) Prosper Otemuyiwa Tue, 10 Mar 2026 12:13:15 +0000 https://dev.to/valyuai/perplexity-sonar-alternatives-for-developers-2026-8j3 https://dev.to/valyuai/perplexity-sonar-alternatives-for-developers-2026-8j3 <p><strong>Quick Answer:</strong> The best alternatives to Perplexity Sonar for developers are Valyu (for specialised data access + web search + benchmark-leading accuracy), Linkup (for simple web search with predictable pricing), and Tavily (for LangChain/LlamaIndex-integrated RAG). </p> <p>For Sonar Deep Research specifically, Valyu's <a href="proxy.php?url=https://docs.valyu.ai/guides/deepresearch" rel="noopener noreferrer">DeepResearch API</a> is the only alternative that matches multi-step research with citations while also accessing full-text SEC filings, PubMed, and academic sources that Sonar cannot reach.</p> <p>I've been building search-grounded AI apps long enough to have a mental list of the API switches that felt obvious in hindsight. Switching off Perplexity Sonar is near the top.</p> <p>The Sonar API is a capable product. I'm not here to trash it. But if you've been running a production app on Sonar and you've hit the reliability ceiling, the throttling wall, or the "this thing only knows about public web pages" limit, you're not alone, and there are real alternatives now.</p> <p>This article covers what Perplexity Sonar actually is (the full product family, not just the consumer chatbot), the documented reasons developers switch, and the concrete alternatives for each use case, including a dedicated section on Sonar Deep Research specifically.</p> <h2> What Perplexity Sonar Actually Is </h2> <p>First: Sonar is Perplexity's API product, not the consumer app. </p> <p>"Perplexity" and "Sonar" are distinct things. Searching for Perplexity alternatives usually returns lists of consumer chatbots (ChatGPT, Claude, etc.). That's the wrong category if you're building as a developer.</p> <p>The Sonar API family has five models:</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Model</th> <th>Primary Use</th> <th>Pricing</th> </tr> </thead> <tbody> <tr> <td>sonar</td> <td>Lightweight Q&amp;A, high-volume</td> <td>$5/1,000 requests</td> </tr> <tr> <td>sonar-pro</td> <td>Deeper content understanding</td> <td>$8/1,000 requests + tokens</td> </tr> <tr> <td>sonar-reasoning-pro</td> <td>Enhanced multi-step reasoning</td> <td>$2/M input, $8/M output</td> </tr> <tr> <td>sonar-deep-research</td> <td>Exhaustive multi-step research reports</td> <td>$2/M input, $8/M output + $5/1,000 searches + $3/M reasoning tokens</td> </tr> </tbody> </table></div> <p><strong>sonar-deep-research</strong> runs autonomous multi-step research, searches the web multiple times, reasons over what it finds, produces a comprehensive report. Available via Perplexity API and OpenRouter.</p> <p>The pricing for Deep Research compounds fast. A request that triggers 30 searches costs $0.15 in search fees alone, before tokens.</p> <h2> Why Developers Are Switching </h2> <p>These are documented complaints from <a href="proxy.php?url=https://www.reddit.com/r/perplexity_ai/" rel="noopener noreferrer">/r/perplexity_ai</a>, <a href="proxy.php?url=https://www.reddit.com/r/AI_Agents" rel="noopener noreferrer">/r/AI_Agents</a>, and developer comparisons, not personal opinions.</p> <p><strong>Reliability.</strong> One developer documented 20+ daily API outages with the Perplexity status page showing green the entire time. For a production app, that's not workable. The common workaround is building fallback logic, which defeats the point of paying for a managed API.</p> <p><strong>Intentional throttling.</strong> Linkup documented this in their own comparison: Sonar's API accuracy appears deliberately capped to avoid creating a competitive consumer product. Perplexity's primary business is a consumer chatbot. The API is secondary. The engineering priorities show.</p> <p><strong>The architecture problem.</strong> Sonar uses a <code>/chat/completions</code> endpoint, the same pattern as LLM chat APIs. Every API call forces a full text generation. If you only want source URLs for a RAG pipeline, you still pay for a generated answer. For developers who want to control the generation step themselves (use Claude or GPT-4 for generation, use search just for retrieval), this creates a wasteful and expensive architecture. </p> <p>Linkup measured this...The unpredictability in output token length (ranging from 4 to 340,000 tokens) makes cost-per-query essentially impossible to forecast.</p> <p><strong>Web-only.</strong> This is the hard ceiling. Sonar searches the public web. That's it. No SEC filings, no PubMed, no academic journals, no ChEMBL compound databases, no FRED economic data. If you're building financial analysis tools, biomedical research assistants, or anything that requires authoritative data that lives behind institutional barriers, Sonar hits a wall.</p> <p><strong>The $5 Pro plan trap.</strong> Many developers sign up for Perplexity Pro at $20/month expecting meaningful API access. The Pro plan includes $5/month in API credits. That's roughly 1,000 standard queries at low search depth. A heavy testing session can burn through it in hours.</p> <h2> Alternatives for Standard Sonar Use Cases </h2> <h3> Valyu DeepSearch API </h3> <p>The most differentiated option in this category, specifically because it goes beyond web search.</p> <p>Valyu's <a href="proxy.php?url=https://docs.valyu.ai/guides/deepresearch" rel="noopener noreferrer">DeepSearch API</a> gives you a single endpoint that searches the public web AND 36+ specialised data sources; SEC 10-K, 10-Q, 13F, 13D, 13G filings with full-text search, PubMed and bioRxiv research papers, ChEMBL bioactive compounds, academic journals, FRED and BLS economic data, clinical trials, patent databases.</p> <p>For developers building anything that needs financial data, biomedical research, academic content, or economic indicators, this is the alternative that actually solves the problem. Sonar doesn't provide reliable data in many of these areas.</p> <p>On raw web search accuracy, Valyu benchmarks at 79% on FreshQA (600 time-sensitive queries) versus Sonar's architecture which relies on Perplexity's indexed web. </p> <p>On finance-specific questions, Valyu scores 73% vs Google's 55%.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="kn">from</span> <span class="n">valyu</span> <span class="kn">import</span> <span class="n">Valyu</span> <span class="n">client</span> <span class="o">=</span> <span class="nc">Valyu</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="sh">"</span><span class="s">your-api-key</span><span class="sh">"</span><span class="p">)</span> <span class="c1"># Search web + SEC filings + economic data in one call </span><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">What risk factors did Apple disclose in their most recent 10-K?</span><span class="sh">"</span><span class="p">,</span> <span class="n">search_type</span><span class="o">=</span><span class="sh">"</span><span class="s">all</span><span class="sh">"</span><span class="p">,</span> <span class="c1"># web + proprietary </span> <span class="n">max_num_results</span><span class="o">=</span><span class="mi">10</span> <span class="p">)</span> <span class="nf">print</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> </code></pre> </div> <p>The <a href="proxy.php?url=https://docs.valyu.ai/integrations/mcp-server#remote-mcp" rel="noopener noreferrer">MCP server integration</a> means it drops into Claude Desktop, Cursor, and other MCP-compatible tools with zero additional code. <a href="proxy.php?url=https://docs.valyu.ai/integrations/vercel-ai-sdk#vercel-ai-sdk" rel="noopener noreferrer">Native Vercel AI SDK</a> and <a href="proxy.php?url=https://docs.valyu.ai/integrations/langchain#langchain-integration" rel="noopener noreferrer">LangChain</a> integrations exist.</p> <p>Platform: <a href="proxy.php?url=https://platform.valyu.ai" rel="noopener noreferrer">platform.valyu.ai</a> | Docs: <a href="proxy.php?url=https://docs.valyu.ai/home" rel="noopener noreferrer">docs.valyu.ai</a> | Free $10 credit on signup.</p> <p><strong>Best for</strong>: Developers who need specialised data (financial, biomedical, academic, economic) or who want benchmark-leading accuracy on time-sensitive and domain-specific queries.</p> <h3> Linkup </h3> <p>Linkup's pitch is architectural clarity. They expose a dedicated <code>/search</code> endpoint with an <code>outputType</code> parameter, you specify whether you want <code>search_results</code> (clean JSON of sources), <code>answer</code> (generated text), or <code>structured</code> (custom JSON schema). This is fundamentally different from Sonar's chat completion approach.</p> <p>Pricing is transparent and flat: standard search at €5/1,000 queries, deep search at €50/1,000 queries. No token variability. On SimpleQA benchmarks, Linkup hit 91% vs Sonar's 77.3%.</p> <p>The one limitation: Linkup is web-only. No specialised data access.</p> <p><strong>Best for</strong>: Developers who want predictable pricing and architectural control over the search/generation split.</p> <h3> Tavily </h3> <p>They are one of the most commonly used search APIs in the LangChain/LlamaIndex ecosystem. Tavily's biggest advantage is installation friction: if you're building a LangChain agent, Tavily is a one-liner. The free tier (1,000 credits/month) is generous enough for development.</p> <p>Performance is solid. Pricing: $0.008/credit pay-as-you-go, Monthly plans start around $30/month for ~4,000 credits.</p> <p><strong>Best for</strong>: RAG pipelines built on LangChain or LlamaIndex where developer experience and quick integration matter more than maximum accuracy.</p> <h2> Alternatives for Sonar Deep Research Specifically </h2> <p><strong>sonar-deep-research</strong> occupies a specific niche: multi-step autonomous research that produces comprehensive reports, not just answers. The use case is different from standard search. You're asking it to do what a human researcher would do over an hour, not just answer a question.</p> <p>The key question is what you actually need from a deep research API:</p> <ol> <li>Multi-step search execution (runs multiple queries, synthesizes results)</li> <li>Citations and source references in the output</li> <li>Access to authoritative data sources, not just indexed web pages</li> <li>Structured output or webhook support for long-running tasks</li> </ol> <p>On criteria 1, 2, and 3, <strong>Valyu's DeepResearch API</strong> is the only alternative that matches <strong>sonar-deep-research</strong> functionally while expanding what it can do.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="c1"># Valyu DeepResearch - multi-step autonomous research # with access to SEC filings, PubMed, academic journals </span> <span class="kn">from</span> <span class="n">valyu</span> <span class="kn">import</span> <span class="n">Valyu</span> <span class="n">client</span> <span class="o">=</span> <span class="nc">Valyu</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="sh">"</span><span class="s">YOUR_API_KEY</span><span class="sh">"</span><span class="p">)</span> <span class="c1"># Create an async DeepResearch task </span><span class="n">task</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Analyze the financial health of Tesla based on recent SEC filings and analyst sentiment</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">standard</span><span class="sh">"</span><span class="p">,</span> <span class="c1"># also supports: "fast", "heavy", "max" </span> <span class="n">output_formats</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">markdown</span><span class="sh">"</span><span class="p">],</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">finance</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">academic</span><span class="sh">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">)</span> <span class="k">if</span> <span class="n">task</span><span class="p">.</span><span class="n">success</span><span class="p">:</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Task created: </span><span class="si">{</span><span class="n">task</span><span class="p">.</span><span class="n">deepresearch_id</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="c1"># Wait for completion with progress updates </span> <span class="n">result</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">wait</span><span class="p">(</span> <span class="n">task</span><span class="p">.</span><span class="n">deepresearch_id</span><span class="p">,</span> <span class="n">on_progress</span><span class="o">=</span><span class="k">lambda</span> <span class="n">s</span><span class="p">:</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Status: </span><span class="si">{</span><span class="n">s</span><span class="p">.</span><span class="n">status</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="p">)</span> <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">status</span> <span class="o">==</span> <span class="sh">"</span><span class="s">completed</span><span class="sh">"</span><span class="p">:</span> <span class="nf">print</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">output</span><span class="p">)</span> <span class="c1"># markdown report </span> <span class="nf">print</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">sources</span><span class="p">)</span> <span class="c1"># cited sources with URLs </span> <span class="nf">print</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">cost</span><span class="p">)</span> <span class="c1"># fixed task cost </span></code></pre> </div> <p>The difference that matters for serious use cases: when Sonar Deep Research researches "clinical trial outcomes for GLP-1 receptor agonists," it's searching whatever's publicly indexed on the web. When Valyu DeepResearch does the same query, it's searching PubMed, ClinicalTrials.gov, FDA drug labels, ChEMBL, and Open Target where the actual source databases where this research lives.</p> <p>For financial research: Valyu reads the actual 10-K filings, earnings transcripts, and insider trading data. Sonar reads articles about those filings.</p> <p><strong>Pricing comparison for deep research:</strong></p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Product</th> <th>Per Request Cost</th> <th>Data Sources</th> <th>Output</th> </tr> </thead> <tbody> <tr> <td>Sonar Deep Research</td> <td>$2/M input + $8/M output + $5/1,000 searches + $3/M reasoning tokens</td> <td>Public web only</td> <td>Markdown report</td> </tr> <tr> <td>Valyu DeepResearch (fast)</td> <td>$0.10</td> <td>Web + 36+ specialised sources</td> <td>Markdown, PDF, JSON</td> </tr> <tr> <td>Valyu DeepResearch (standard)</td> <td>~$1-3</td> <td>Web + 36+ specialised sources</td> <td>Markdown, PDF, JSON</td> </tr> <tr> <td>Valyu DeepResearch (max)</td> <td>$15.00</td> <td>Web + 36+ specialised sources</td> <td>Markdown, PDF, JSON, Excel</td> </tr> </tbody> </table></div> <p>Valyu also supports <a href="proxy.php?url=https://docs.valyu.ai/guides/deepresearch#webhooks" rel="noopener noreferrer">webhooks</a> (for async long-running tasks), file analysis, and follow-up instructions, features the Sonar Deep Research endpoint doesn't offer.</p> <h2> Head-to-Head Comparison Table </h2> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th></th> <th>Perplexity Sonar</th> <th>Valyu DeepSearch</th> <th>Linkup</th> <th>Tavily</th> </tr> </thead> <tbody> <tr> <td><strong>Web search</strong></td> <td>Yes</td> <td>Yes</td> <td>Yes</td> <td>Yes</td> </tr> <tr> <td><strong>Proprietary data</strong></td> <td>No</td> <td>36+ sources</td> <td>No</td> <td>No</td> </tr> <tr> <td><strong>FreshQA accuracy</strong></td> <td>Not published</td> <td>79%</td> <td>Not published</td> <td>Not published</td> </tr> <tr> <td><strong>SimpleQA accuracy</strong></td> <td>77–86%</td> <td>94%</td> <td>91%</td> <td>Not published</td> </tr> <tr> <td><strong>Dedicated search endpoint</strong></td> <td>No (/chat only)</td> <td>Yes</td> <td>Yes</td> <td>Yes</td> </tr> <tr> <td><strong>MCP integration</strong></td> <td>No</td> <td>Yes</td> <td>No</td> <td>No</td> </tr> <tr> <td><strong>LangChain integration</strong></td> <td>Limited</td> <td>Yes</td> <td>Yes</td> <td>Yes (native)</td> </tr> <tr> <td><strong>Webhooks</strong></td> <td>No</td> <td>Yes (DeepResearch)</td> <td>No</td> <td>No</td> </tr> <tr> <td><strong>Predictable pricing</strong></td> <td>No (token-variable)</td> <td>Yes</td> <td>Yes</td> <td>Yes</td> </tr> <tr> <td><strong>Deep research mode</strong></td> <td>Yes (sonar-deep-research)</td> <td>Yes (DeepResearch API)</td> <td>Yes (deep search)</td> <td>No</td> </tr> <tr> <td><strong>Entry price (web search)</strong></td> <td>$5/1,000 requests</td> <td>Free $10 credit</td> <td>€5/1,000 queries</td> <td>Free 1k credits/mo</td> </tr> </tbody> </table></div> <h2> Which Alternative for Which Use Case </h2> <p><strong>Building a RAG pipeline on LangChain/LlamaIndex?</strong><br> Start with Tavily. It's the path of least resistance and performs well. If you hit accuracy issues or need data beyond the public web, move to Valyu.</p> <p><strong>Building anything that touches financial data?</strong><br> Valyu. SEC filings, earnings data, stock prices, FRED economic data, balance sheets, insider trading, all in one API call. No other search API comes close for this use case.</p> <p><strong>Building biomedical or clinical research tools?</strong><br> Valyu. PubMed, bioRxiv, ClinicalTrials.gov, ChEMBL, DrugBank, Open Targets, FDA drug labels. The <a href="proxy.php?url=https://bio.valyu.ai" rel="noopener noreferrer">Bio app</a> (310 GitHub stars) is a live demo of what's possible.</p> <p><strong>Need multi-step autonomous research (replacing sonar-deep-research)?</strong><br> Valyu DeepResearch API. It's the only deep research API that combines multi-step synthesis with access to authoritative specialised databases, plus structured output formats and webhook support.</p> <p><strong>Pure web search with transparent pricing, no specialised data needed?</strong><br> Linkup for pricing predictability.</p> <h2> A Note on What Sonar Does Well </h2> <p>Sonar has strong developer documentation, an OpenAI-compatible API format that makes migration easy, and solid performance for general web Q&amp;A tasks. The <code>sonar-reasoning-pro</code> model is genuinely useful for chain-of-thought web-grounded reasoning.</p> <p>The limitations documented here are real, but they're architectural constraints that come from being a secondary product of a consumer AI company, most likely not signs of bad engineering. If your use case is pure web Q&amp;A at scale and you're happy with the public web as your data universe, Sonar is a viable choice.</p> <p>The alternatives above exist for when those constraints become blockers.</p> <h2> FAQ </h2> <p><strong>What is the best alternative to Perplexity Sonar API?</strong><br> Valyu DeepSearch for developers who need specialised data access (finance, biomedical, academic) or benchmark-leading accuracy. Linkup for predictable pricing and architectural control. Tavily for the fastest LangChain/LlamaIndex integration.</p> <p><strong>What is the best alternative to Perplexity Sonar Deep Research?</strong><br> Valyu's DeepResearch API is the only alternative that matches sonar-deep-research on multi-step research synthesis while also accessing SEC filings, PubMed, academic journals, and clinical trial data that Sonar cannot reach.</p> <p><strong>Is Perplexity Sonar web-only?</strong><br> Yes. All Sonar models (Sonar, Sonar Pro, Sonar Reasoning, Sonar Reasoning Pro, Sonar Deep Research) search only the public web and Perplexity's index. They have no access to SEC filings, academic databases, clinical trials, or other specialised data sources.</p> <p><strong>Why is Sonar API unreliable?</strong><br> Perplexity's primary product is a consumer chatbot. The API is a secondary product. Developer forums document 20+ daily outages with no reflection on the official status page. The API capacity and reliability engineering reflects these priorities.</p> <p><strong>Does Perplexity Sonar have an MCP integration?</strong><br> Yes. Sonar offers a Model Context Protocol server. Valyu also does. It can be added to Claude Desktop, Cursor, and other MCP-compatible environments with one command: <strong>npx skills add valyuAI/skills</strong>.</p> <p><strong>How does sonar-deep-research pricing work?</strong><br> Sonar Deep Research charges $2/million input tokens, $8/million output tokens, $5 per 1,000 searches (a single request might trigger 30 searches = $0.15 in search fees alone), and $3/million reasoning tokens. The total cost per request is hard to predict and can range significantly based on query complexity.</p> sonar sonardeepresearchalternatives deepresearch perplexity What is AI Search? Prosper Otemuyiwa Wed, 04 Mar 2026 12:10:07 +0000 https://dev.to/valyuai/what-is-ai-search-2d0o https://dev.to/valyuai/what-is-ai-search-2d0o <p><em>A developer's guide to understanding AI-native search, how it works, what separates the good from the bad, and what to actually check before picking a provider.</em></p> <p><strong>Quick answer:</strong> AI search is the ability for an AI system to query external information sources at runtime and retrieve actual content. Not just links, not summaries of summaries, but real data. Without it, an LLM is limited to whatever it saw during training. With it, an LLM can answer questions about earnings calls filed yesterday, drug interactions from the latest clinical trial, or stock prices from three hours ago. <br> AI search is to an LLM what the internet is to a knowledge worker. It's not optional infrastructure.</p> <h2> Table of Contents </h2> <ol> <li>Why AI Agents Need Search</li> <li>AI-Native Search vs Traditional Keyword Search</li> <li> The Five Things That Need to Be First-Class <ul> <li>Breadth: Web + Proprietary Sources</li> <li>Depth: Content, Not Links</li> <li>Freshness: Real-Time, Not Stale Caches</li> <li>AI-Native Query Understanding</li> <li>LLM Integration: First-Class, Not Bolted On</li> </ul> </li> <li>The Evaluation Checklist</li> <li>The Good, and the Bad</li> <li>Benchmark Reality Check</li> <li>FAQ</li> </ol> <h2> Why AI Agents Need Search </h2> <p>Think about what makes a human researcher effective. It is not just memory, it is the ability to go look things up. </p> <p>A doctor does not rely purely on what they memorized in medical school. They check <a href="proxy.php?url=https://www.uptodate.com" rel="noopener noreferrer">UpToDate</a>, <a href="proxy.php?url=https://pubmed.ncbi.nlm.nih.gov" rel="noopener noreferrer">PubMed</a>, current prescribing guidelines.</p> <p>A financial analyst does not rely on their training data. They pull the latest 10-K, check earnings transcripts, cross-reference FRED data, check stocks daily.</p> <p>LLMs face the same constraint. GPT-4o was trained on data with a cutoff. Same with Claude and Gemini. Every model's knowledge stops somewhere. This creates three categories of failure:</p> <ol> <li> <strong>Staleness</strong>: Asking about anything that changed after the training cutoff returns either a wrong answer or a hedge ("I don't have information beyond...").</li> <li> <strong>Hallucination at the edges</strong>: When a model is uncertain, it sometimes fills the gap with plausible-sounding fiction. Real-time retrieval with citations is the structural fix for this.</li> <li> <strong>Coverage gaps</strong>: Training data is biased toward publicly crawlable content. SEC filings, paywalled research papers, proprietary financial data, clinical trial databases, most of the professional information infrastructure does not end up in training data at useful fidelity.</li> </ol> <p>Search solves all three. Not by making the model smarter in the abstract, but by giving it access to ground truth at query time.</p> <p>The pattern that works: LLM receives a question, determines it needs external data, calls a search tool, retrieves real content, reasons over that content, returns a <strong>grounded answer with citations</strong>. This is the architecture that drives production AI applications in finance, healthcare, legal, transportation and research today.</p> <h2> AI-Native Search vs Traditional Keyword Search </h2> <p>This distinction matters more than most developers realize when they first start building.</p> <p>Traditional search: The kind behind every enterprise search box built in the past decade and more. It operates on keyword matching. You construct a query like <code>cancer AND (immunotherapy OR checkpoint inhibitor) NOT pediatric</code> and get documents that contain those terms. This works for humans who have time to iterate on queries, scan results, and synthesize across ten browser tabs.</p> <p>If you have spent any time doing serious research on Google, you know the tricks that have accumulated over the years: <code>site:gov filetype:pdf</code> to find government PDFs, <code>intitle:"annual report" "2024"</code> to find exact-title matches, <code>"exact phrase" -exclusion</code> to filter noise, <code>after:2024-01-01</code> to constrain by date, or <code>related:competitor.com</code> to find similar sites. Power users have entire mental libraries of these operators. I became an expert at these tricks. Some queries end up looking like:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>"PFAS contamination" site:epa.gov OR site:atsdr.cdc.gov filetype:pdf after:2023-01-01 -"press release" </code></pre> </div> <p>This is not a quirk. It is part of the fundamental design. Google was built for humans who can iteratively refine, scan results, and make judgment calls about relevance. The operator syntax is the escape hatch that power users reach for when natural language search fails them.</p> <p>AI agents do not work like that. They work like this:</p> <blockquote> <p>"Get me the stock price of Tesla over the last 30 days"</p> <p>"What did Pfizer's CFO say about margins in their most recent earnings call?"</p> <p>"Which clinical trials are currently recruiting for NASH treatment in the US?"</p> <p>"Get me the rulings and judgements about insider trading that happened in the past 20 days"</p> </blockquote> <p>These are natural language questions. They imply intent. They require the search layer to understand that <strong>"last 30 days"</strong> means a dynamic date range, that <strong>"earnings call"</strong> means looking at SEC filings or earnings transcripts, that <strong>"currently recruiting"</strong> means filtering on trial status.</p> <p>An LLM generating a Google query has to translate intent into operator syntax before it can search and then translate SERP results back into content before it can reason. Every translation step introduces error. </p> <p>An LLM that generates <code>NASH clinical trials recruiting site:clinicaltrials.gov</code> might get results, or it might not, depending on how <strong>clinicaltrials.gov</strong> structures its content for Google's crawler. The search layer is working against the LLM, not with it.</p> <p>An AI-native search system handles this natively. You pass natural language, the kind of query an LLM would generate, and the search layer handles the translation to underlying sources, retrieves actual content (not a list of URLs), and returns that content in a format the LLM can reason over directly.</p> <p>The shift from keyword search to AI-native search is roughly similar to the shift from SQL to natural language database queries. The interface changes, the underlying capability requirements change, and the failure modes change completely.</p> <h2> The Five Things That Need to Be First-Class </h2> <p>When evaluating an AI search provider, five things matter enough that weakness in any one of them might be a dealbreaker for serious use cases.</p> <h3> 1. Breadth: Web + Proprietary Sources </h3> <p>Most AI search APIs search the web. That is the <strong>table stakes</strong>. What separates research-grade search from ordinary search is proprietary source coverage.</p> <p>The information that actually matters in professional contexts is mostly not just sitting on the open web:</p> <ul> <li> <strong>Financial research</strong>: SEC filings (10-Ks, 10-Qs, 8-Ks), earnings transcripts, balance sheets, insider trading disclosures - these are on EDGAR but require structured access to be useful for AI</li> <li> <strong>Biomedical research</strong>: PubMed, bioRxiv, medRxiv, clinical trial registries, ChEMBL's 2.5 million bioactive compounds, DrugBank, FDA drug labels</li> <li> <strong>Academic research</strong>: Full-text multimodal search over ArXiv, PubMed, BioRxiv, MedRxiv, and more. The actual papers, not just the abstracts</li> <li> <strong>Economic data</strong>: FRED (Federal Reserve Economic Data), BLS statistics, World Bank indicators</li> <li> <strong>Legal and regulatory</strong>: Patent databases, SEC enforcement actions, legislation</li> </ul> <p>But public databases are only part of the picture. The other category of proprietary data is internal, and it is often where the highest-value information lives.</p> <ul> <li>A mid-size law firm's decades of case notes, client briefs, and research memos. </li> <li>A pharmaceutical company's internal compound testing database that has never been published. </li> <li>A logistics company's shipment history and carrier performance data. An enterprise sales team's CRM notes and deal history. None of this is on the open web. None of it is in any public database. </li> </ul> <p>But all of it is the kind of context that makes AI responses actually useful for the people inside those organizations.</p> <p>The right AI search architecture can be plugged into these internal sources too: vector databases, document stores, internal wikis, SQL databases, proprietary APIs. </p> <p>The more sources a search layer can reach, the more complete its picture of the world. An AI assistant that can simultaneously search PubMed, a biotech company's internal research database, and the latest FDA filings delivers fundamentally different answers than one that is web-only.</p> <p>This is the <strong>compounding advantage of breadth</strong>: each additional source does not just add coverage, it adds the ability to cross-reference. <strong>"Find mentions of compound X across our internal trial data, published literature, and competitor patent filings"</strong> requires all three sources to be reachable in one query. Web-only search makes that impossible by design.</p> <p>When evaluating breadth, ask specific questions:</p> <ul> <li>Which proprietary sources are integrated, and what is the specific dataset (not just "financial data" but "SEC 10-K filings with full text including MD&amp;A sections")?</li> <li>Are academic papers full-text or abstract-only?</li> <li>How many data sources are covered, and can you filter by source type in a single API call?</li> <li>Does the provider support connecting to custom internal sources, and through what mechanism?</li> </ul> <p>Web-only providers and <em>most of the market is web-only</em> will fail any use case that requires professional-grade data, internal knowledge, or cross-source synthesis.</p> <h3> 2. Depth: Content, Not Links </h3> <p>This is the difference between a search API and a web search API wrapper.</p> <p>A web search wrapper returns a list of URLs with snippets. The LLM then has to decide which links are worth following, potentially trigger additional API calls to retrieve content, and synthesize across multiple round trips. This is slow, expensive, and introduces noise.</p> <p>AI-native search returns content directly. When you query for "Moderna's RNA platform approach in their 2024 10-K," you should get the actual text from that document, the specific section about their RNA platform, not just a URL to EDGAR where you could theoretically find that document.</p> <p>Depth also means content quality. The raw HTML of most financial documents is a mess. PDFs are worse. A good search provider handles extraction, normalization, and structuring as part of the retrieval pipeline. You get clean, LLM-ready text, not a soup of HTML tags and formatting artifacts.</p> <p><strong>Depth indicators to check:</strong></p> <ul> <li>Does the API return full document content or snippets?</li> <li>What is the content extraction quality on complex document types (PDFs, tables, structured financial data)?</li> <li>What is the maximum content length per result?</li> <li>Is there a content extraction endpoint that works separately from search?</li> </ul> <h3> 3. Freshness: Real-Time, Not Stale Caches </h3> <p><em>This is where the difference between providers becomes visible in production.</em></p> <p>Heavy crawl caching was a reasonable approach for search engines built for human consumption. If a document was crawled three weeks ago, that is probably fine for general-purpose search. For AI agents operating in time-sensitive contexts, it is a <strong>critical failure mode</strong>.</p> <p>A recruiter using an AI research tool should not discover that the candidate's current employer is wrong because the AI's search layer cached their LinkedIn profile three months ago. A financial analyst querying recent news should not get results from last month because the search provider's crawl queue is backed up.</p> <p>The worst pattern is a search provider that advertises "live search" but performs live crawls only as a fallback when the cached version is stale by their internal definition. The result is non-deterministic freshness: sometimes you get today's data, sometimes you get data from three weeks ago, and you cannot tell which you are getting without manual verification.</p> <p><strong>Real freshness requirements:</strong></p> <ul> <li> <strong>News and market data</strong>: Minutes, not hours</li> <li> <strong>SEC filings</strong>: EDGAR indexes within 5-10 minutes of filing; your search should reflect this</li> <li> <strong>Clinical trial registries</strong>: Updates happen continuously. Staleness of more than 24 hours affects research validity</li> <li> <strong>Web content</strong>: Varies by use case, but the provider should give you visibility into crawl timestamps</li> </ul> <p>When evaluating a provider's freshness claims, ask for documentation of their crawl frequency and caching policy. Ask whether their "live crawl" option is reliable or an unreliable fallback. Run the same query twice on the same day with different time stamps in the query, and check whether results change.</p> <h3> 4. AI-Native Query Understanding </h3> <p>The query interface is the developer experience.</p> <p>Legacy search APIs require you to construct the query in a format the search engine understands: keyword boolean syntax, specific field names, structured filters. You have to translate the user's intent into the search system's dialect before you can use it.</p> <p>AI-native search inverts this. You pass natural language, and the search layer handles the translation to underlying sources. This matters across every <strong>vertical</strong>:</p> <p><strong>Finance</strong>: "What did Tesla's management say about gross margin pressure in the last two earnings calls?" should route to earnings transcripts and SEC filings, not a news summary blog.</p> <p><strong>Biomedical</strong>: "Recent studies on CRISPR off-target effects in vivo" should pull from PubMed and bioRxiv, ranked by recency and citation weight.</p> <p><strong>Legal</strong>: "UK court precedents on contractor misclassification in the gig economy since 2020" should pull from case law databases with correct jurisdictional filtering, not general web results that might cite the wrong legal system or be two years out of date.</p> <p><strong>Shipping and logistics</strong>: "Current Suez Canal transit delays for container ships" should route to live maritime tracking data, port authority feeds, and recent news, not a Wikipedia article about the 2023 blockage.</p> <p><strong>Prediction markets</strong>: "Current odds on the next Federal Reserve rate decision across Polymarket and Kalshi" should pull from those specific markets with live contract prices, not a news article from last week speculating about what the Fed might do.</p> <p>Each of these queries contains <strong>implicit routing logic</strong>: which sources are relevant, what time frame applies, what the user actually means by vague terms like "recent" or "current." AI-native search handles this inference layer so the LLM does not have to.</p> <p>Semantic understanding also matters for result ranking. A keyword search for "risk factors" returns documents containing the phrase "risk factors." </p> <p>A semantically-aware search returns documents that discuss risk even if the exact phrase does not appear because the model understands that "material uncertainty," "contingent liabilities," and "regulatory exposure" are semantically proximate to the concept of risk.</p> <p><strong>Test this concretely:</strong> Give a provider three natural language queries you would realistically generate from an LLM tool call. Evaluate whether the results are actually what those queries mean, not just documents that contain the keywords.</p> <h3> 5. LLM Integration: First-Class, Not Bolted On </h3> <p>How the search API integrates into your AI stack determines the actual developer experience.</p> <p><strong>First-class integration means</strong>:</p> <ul> <li> <strong>Tool/function calling format</strong>: The API should work as a native tool in OpenAI, Anthropic, and other LLM tool-use patterns. You define the tool once and the LLM decides when to call it and what to pass.</li> <li> <strong>Framework support</strong>: LangChain, LlamaIndex, Vercel AI SDK, CrewAI - Your search provider should be a first-class citizen in whichever orchestration layer you use. Not a custom wrapper you have to maintain.</li> <li> <strong>MCP support</strong>: Model Context Protocol is now the standard for LLM-to-tool communication. A provider without MCP support adds friction for Claude and Cursor users.</li> <li> <strong>Streaming</strong>: For real-time interfaces, the ability to stream results as they arrive matters for perceived performance.</li> <li> <strong>Structured outputs</strong>: The LLM often needs search results in a specific schema. Can the search provider return structured JSON directly, or do you have to parse raw text in your application layer?</li> </ul> <p>Bolted-on integration looks like this in practice:</p> <ul> <li>A REST API that returns a single blob of text with no schema, so you have to write custom parsing logic in your application to extract titles, URLs, content, and timestamps separately.</li> <li>No official SDK or not enough SDKs. You are writing raw HTTP requests or maintaining your own wrapper library that breaks every time the provider changes their response format.</li> <li>Documentation that shows Python examples only, nothing for TypeScript or Go, and the Python examples use <code>requests</code> instead of an official client.</li> <li>MCP support that is "in beta" with no ETA, forcing you to write a custom MCP server adapter.</li> <li>No streaming support, so your UI freezes for 3-5 seconds on every search call while waiting for the full response.</li> <li>LangChain integration that exists as a third-party community package maintained by one person, not the search provider.</li> <li>No tool-use JSON schema provided, meaning you have to write your own OpenAI function definition and hope it matches what the API actually accepts.</li> <li>Rate limit errors that return HTTP 200 with an error field in the body instead of HTTP 429, breaking standard retry logic.</li> <li>Pagination that requires stateful session tokens the LLM cannot manage across tool calls.</li> </ul> <p>The cumulative effect of bolted-on integration is that you spend engineering time maintaining glue code rather than building your product. This is the quiet cost that does not show up in a pricing comparison but adds up to weeks of engineering time at scale.</p> <h2> The Evaluation Checklist </h2> <p>Use this when running a structured evaluation of AI search providers:</p> <p><strong>Data Coverage</strong></p> <ul> <li>[ ] Web search included (table stakes)</li> <li>[ ] Which proprietary sources are included, listed specifically, not categorically</li> <li>[ ] Full-text access to academic papers (not just abstracts)</li> <li>[ ] Financial data: SEC filings, earnings, market data</li> <li>[ ] Biomedical: PubMed, clinical trials, compound databases</li> <li>[ ] Can you filter by source type in a single API call?</li> <li>[ ] Does the provider support connecting to internal/custom data sources?</li> </ul> <p><strong>Content Quality</strong></p> <ul> <li>[ ] Returns full document content, not just URLs or snippets</li> <li>[ ] Clean extraction from PDFs and structured documents</li> <li>[ ] Table and structured data handling</li> <li>[ ] Configurable result length</li> </ul> <p><strong>Freshness</strong></p> <ul> <li>[ ] Documented crawl frequency per source type</li> <li>[ ] Time-to-index for SEC filings (should be under 30 minutes)</li> <li>[ ] News freshness (should be under 5 minutes for major events)</li> <li>[ ] No silent caching that makes freshness non-deterministic</li> </ul> <p><strong>Query Interface</strong></p> <ul> <li>[ ] Natural language queries work without manual keyword construction</li> <li>[ ] Semantic ranking, not just keyword matching</li> <li>[ ] Handles multi-part queries correctly</li> </ul> <p><strong>Integration</strong></p> <ul> <li>[ ] Official SDK for your language</li> <li>[ ] LangChain / LlamaIndex integration</li> <li>[ ] Vercel AI SDK tool integration</li> <li>[ ] MCP server support</li> <li>[ ] Streaming support</li> </ul> <p><strong>Reliability and Pricing</strong></p> <ul> <li>[ ] SLA with documented uptime</li> <li>[ ] Pricing is per-result, not per-request (so you pay for what you get)</li> <li>[ ] Rate limits are documented and sufficient for your use case</li> <li>[ ] Transparent pricing for proprietary vs web content</li> </ul> <h2> The Good, and the Bad </h2> <h3> The Good </h3> <p><strong>Unified search across heterogeneous sources.</strong> The right architecture gives you a single API call that can query the web, SEC filings, PubMed, and FRED economic data simultaneously. Your LLM does not need to know which source to use for which question. The search layer figures that out. This is transformative for building research agents: instead of wiring together five different data source integrations, you have one.</p> <p><strong>Grounded answers that cite sources.</strong> When search results are the context for LLM reasoning, the LLM can cite its sources. This changes the trust model completely. A financial analyst can see not just the answer but the specific 10-K paragraph that supports it. A doctor can see which PubMed study the recommendation came from. <em>Grounded answers are auditable; pure LLM answers are not.</em></p> <p><strong>Real-time awareness.</strong> An LLM with search access is always current. It does not need to be retrained to know about yesterday's earnings call or last week's FDA ruling. This decouples knowledge currency from model versioning, which is a significant architectural win.</p> <p><strong>Reduced hallucination on factual claims.</strong> Hallucinations happen most often when the model is uncertain. It generates confident rubbish. Real-time retrieval gives the model <strong>ground truth</strong> to reason over rather than forcing it to infer from training data. Benchmark data consistently shows that retrieval-augmented generation outperforms pure LLM generation on factual tasks: 79% accuracy on FreshQA for top-tier AI search vs 39% for Google's standard search.</p> <h3> The Bad </h3> <p><strong>Latency.</strong> Search adds round-trip time to every query that requires it. A tool call to retrieve content and return it to the LLM typically adds 1-5 seconds depending on source and query complexity. For real-time interfaces, this is noticeable. Deep research workflows that chain multiple search calls can take 30+ seconds. You need to architect around this with streaming, async patterns, and user feedback mechanisms.</p> <p><strong>Cost accumulation.</strong> At scale, search costs add up quickly. Pricing models vary significantly across providers: some charge per request, some per result, some per character of content returned. A single complex research query that touches multiple sources can cost more than you expect if you have not modeled your usage carefully. Price per 1,000 web searches ranges from $1.50 to $15+ depending on the provider and mode.</p> <p><strong>Result noise.</strong> No search system has perfect precision. At high recall settings, you get relevant results but also irrelevant ones. The LLM then has to distinguish signal from noise in its context window and a bloated context with irrelevant content can actually degrade answer quality. Good search configurations tune precision and recall for the specific use case.</p> <p><strong>Over-reliance on search.</strong> Building an AI system that calls search for every query is not always the right architecture. Some queries are better answered from a fine-tuned model or a curated knowledge base. The skill is knowing when to retrieve and when to rely on the model's parametric knowledge. Indiscriminate search adds latency and cost without improving quality for questions the model already knows well.</p> <h2> Benchmark Reality Check </h2> <p>Here is how top AI search providers perform on standardized benchmarks as of early 2026:</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Benchmark</th> <th>Valyu</th> <th>Parallel</th> <th>Exa</th> <th>Google</th> </tr> </thead> <tbody> <tr> <td> <strong>FreshQA</strong> (600 time-sensitive queries)</td> <td><strong>79%</strong></td> <td>52%</td> <td>24%</td> <td>39%</td> </tr> <tr> <td> <strong>SimpleQA</strong> (4,326 factual questions)</td> <td><strong>94%</strong></td> <td>93%</td> <td>91%</td> <td>38%</td> </tr> <tr> <td> <strong>Finance</strong> (120 finance questions)</td> <td><strong>73%</strong></td> <td>67%</td> <td>63%</td> <td>55%</td> </tr> <tr> <td> <strong>Economics</strong> (100 economics questions)</td> <td><strong>73%</strong></td> <td>52%</td> <td>45%</td> <td>43%</td> </tr> <tr> <td> <strong>MedAgent</strong> (562 complex medical queries)</td> <td><strong>48%</strong></td> <td>42%</td> <td>44%</td> <td>45%</td> </tr> </tbody> </table></div> <p><strong>A few things worth noting here:</strong></p> <p>The FreshQA gap between Exa (24%) and top-performing providers (79%) is not a minor implementation difference. It is a fundamental architectural difference in how freshness is handled. Exa's neural search model is built on a large cached index, which delivers excellent semantic relevance on older content but fails on time-sensitive queries. This is a known tradeoff they have publicly acknowledged.</p> <p>The SimpleQA results show that most providers cluster between 91-94% on factual retrieval tasks. The floor drops significantly for Google (38%) because standard Google search is optimizing for human page-browsing behavior, not LLM-ready content delivery.</p> <p>Finance and economics benchmarks show the clearest differentiation, because these domains require proprietary source access that web-only providers do not have. A provider that scores 55% on finance questions versus 73% is not just slower, it is genuinely missing data that lives in structured financial databases rather than on the open web.</p> <h2> FAQ </h2> <p><strong>What is the difference between AI search and RAG?</strong></p> <p>RAG (Retrieval-Augmented Generation) is the broader pattern: retrieve relevant content, add it to the LLM's context, generate an answer. AI search is one implementation of the retrieval component. You can do RAG with a vector database of your own documents, with a search API, or with both. AI search APIs are the external retrieval component, they give your LLM access to content beyond whatever you have locally indexed.</p> <p><strong>Can AI search replace fine-tuning?</strong></p> <p>For knowledge tasks, often yes. Fine-tuning embeds knowledge into model weights, which makes it fast to retrieve but expensive to update. Search retrieves knowledge at query time, which is slower but always current. For a financial assistant that needs to know about last week's earnings call, search is the right tool. For a coding assistant that needs to know your internal coding conventions, fine-tuning or RAG on your codebase is better. Most production systems use both.</p> <p><strong>Why not just use Google Search via the API?</strong></p> <p>Google's Search API returns links and snippets optimized for human web browsing. It does not return full content. It does not cover proprietary databases. It scores 39% on FreshQA despite being the most-crawled index on earth, because its content format and coverage gaps make it poorly suited to LLM context injection. Google is excellent at finding web pages that humans then read. It is not designed for the LLM use case of retrieving ground-truth content for reasoning.</p> <p><strong>What does AI-native search actually mean?</strong></p> <p>It means the search system is designed from the ground up for the query patterns and consumption patterns of AI systems (AI agents, apps, etc) not human users. </p> <p><strong>Key differences:</strong> Natural language queries without keyword syntax, content returned directly rather than URLs to browse, results formatted for LLM context windows, source diversity beyond the open web, and integration patterns (tool calling, MCP, SDK) that fit AI agent architectures.</p> <p><strong>How do I handle freshness requirements in production?</strong></p> <p>First, check timestamp metadata on every search result and log it. This gives you visibility into actual freshness rather than relying on provider claims. <br> Second, separate your use cases by freshness requirement: some queries can tolerate cached results (what is the history of X?), others cannot (what is the current price of X?). </p> <p><strong>What is the right number of search results to pass to an LLM?</strong></p> <p>For most use cases: 3-5 results for single-shot Q&amp;A, 10-15 for research tasks that need broad coverage, 1-2 for fact lookup where precision matters more than recall. The tradeoff is context window cost (more results = more tokens = more cost and potentially degraded coherence) vs recall (fewer results = risk of missing the relevant content). Test empirically on your domain rather than using defaults.</p> <p><strong>Should I build my own search infrastructure or use an API?</strong></p> <p>For domain-specific corpora you own (internal documents, proprietary databases), build or buy a specialized solution. <br> For external information access: Web, SEC filings, academic papers, market data, building your own infrastructure means managing crawl infrastructure, database partnerships, content licensing, and extraction pipelines. This is a multi-year engineering effort. Use an API unless search is literally your core product.</p> <p><strong>What questions should I ask a search provider before signing a contract?</strong></p> <ul> <li>What is your exact crawl frequency for [specific source category relevant to my use case]?</li> <li>Do you have direct database integrations with [specific databases] or do you scrape the web versions?</li> <li>What is the maximum content length I can retrieve per result?</li> <li>What is your SLA and what are the remedies if you miss it?</li> <li>How are proprietary source costs priced? Per retrieval, per query, or per subscription?</li> </ul> ai search aisearch agents Deep Research API for AI Agents: The Complete Guide (2026) Prosper Otemuyiwa Mon, 02 Mar 2026 18:24:14 +0000 https://dev.to/valyuai/deep-research-api-for-ai-agents-the-complete-guide-2026-5bkl https://dev.to/valyuai/deep-research-api-for-ai-agents-the-complete-guide-2026-5bkl <p>I spent three days testing every deep research API I could find. OpenAI's. Perplexity's. Exa's. Parallel's. Gemini's.</p> <p>They all have the same blind spot: they only search the web.</p> <p>If your AI agent needs to cross-reference a drug trial with a <strong>bioRxiv preprint</strong>, or analyze a <strong>company's 10-K</strong> risk factors alongside <strong>FRED economic data</strong>, or map a <strong>patent landscape</strong> against recent academic research, web-only search might not get you there. The data you need most likely isn't indexable by Google or crawlable by search bots.</p> <p>This guide covers what a deep research API actually is, how the major options compare, and how to build AI agents that can reach proprietary data sources: SEC filings, PubMed, clinical trials, patents in a single API call. </p> <p><strong>Note:</strong> All examples are shown in both Python and TypeScript. Non-developers stay tuned, there's also something for you! 😉</p> <h2> What Is a Deep Research API? </h2> <p>A <strong>deep research API</strong> is a programmatic interface that performs multi-step, autonomous research on behalf of an AI agent. Unlike a standard search API that returns a list of results, a deep research API:</p> <ul> <li>Plans a research strategy from a query</li> <li>Executes multiple searches across sources</li> <li>Reads, synthesizes, and cross-references retrieved content</li> <li>Returns a structured report with citations</li> </ul> <p>The term entered mainstream usage after OpenAI launched their <strong>"deep research"</strong> feature in February 2025. Since then, every major AI company has shipped a version. For developers building AI agents, the question isn't whether to use one, it's which one reaches the data you actually need.</p> <p><strong>Key factors when evaluating a deep research API:</strong></p> <ul> <li>Data source coverage (web-only vs. proprietary databases)</li> <li>Output formats (markdown, PDF, structured JSON)</li> <li>The ability to handle deliverables</li> <li>Async handling (how long tasks run, webhook support)</li> <li>Pricing model (per task vs. per retrieval)</li> <li>Benchmark accuracy on domain-specific queries</li> </ul> <h2> The Deep Research API Landscape in 2026 </h2> <p>Here's what's actually ranking when you search "deep research API" and what each tool can and can't reach:</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>API</th> <th>Data Sources</th> <th>Output Formats</th> <th>Best For</th> <th>Pricing</th> </tr> </thead> <tbody> <tr> <td><strong>OpenAI Deep Research</strong></td> <td>Web (Bing)</td> <td>Text</td> <td>General research, broad questions</td> <td>o3: ~$10-30/call ($10/M in, $40/M out); o4-mini: ~$1-3/call ($2/M in, $8/M out)</td> </tr> <tr> <td><strong>Perplexity Deep Research</strong></td> <td>Web</td> <td>Text</td> <td>Quick cited answers</td> <td>Free tier + Pro $20/mo</td> </tr> <tr> <td><strong>Parallel.ai</strong></td> <td>Web</td> <td>Markdown, JSON</td> <td>Developer agentic workflows</td> <td>Per task, usage-based</td> </tr> <tr> <td><strong>Gemini Deep Research</strong></td> <td>Web + Google Search</td> <td>Text, structured</td> <td>Google ecosystem integration</td> <td>Gemini Advanced $20/mo</td> </tr> <tr> <td><strong>Valyu DeepResearch</strong></td> <td>Web + 36+ proprietary (SEC, PubMed, patents, clinical trials, financial data)</td> <td>Markdown, PDF, structured JSON</td> <td>AI agents needing authoritative/paywalled data</td> <td>$0.10-$15.00 per task</td> </tr> </tbody> </table></div> <h3> On pricing transparency </h3> <p>OpenAI's deep research costs are token-based and can spike quickly. One independent analysis ran 10 test queries and spent $100 on o3-deep-research, $9.18 on o4-mini-deep-research. Both are usage-variable, a research task that cites 50 sources will cost substantially more than one that cites 5.</p> <p>Valyu's pricing is flat per task regardless of how many internal searches and retrievals the agent performs. For production workloads where cost predictability matters, that's a meaningful difference.</p> <h3> The critical column: Data Sources </h3> <p>Four of the five options above are web-only. That means:</p> <ul> <li>No SEC 10-K/10-Q filings (EDGAR isn't fully indexable by web crawlers)</li> <li>No paywalled academic papers (Elsevier, Springer, Wiley)</li> <li>No real-time financial data (stock prices, earnings, balance sheets)</li> <li>No clinical trial data (ClinicalTrials.gov full text)</li> <li>No patent claims (USPTO full text)</li> </ul> <p>If your use case lives entirely in open web content, the OpenAI or Perplexity options are fine. If it doesn't, you need an API with proprietary source access.</p> <h2> When Web Search Is Not Enough </h2> <p>Three use cases where web-only deep research fails:</p> <p><strong>1. Financial research agents</strong></p> <p>You ask: "What are the key risk factors disclosed by Nvidia in their latest 10-K, and how do they compare to AMD's?"</p> <p>A web-only API returns news articles <em>about</em> these filings, not the filings themselves. The actual MD&amp;A sections, risk disclosures, and financial statements are in EDGAR. Some might return details about the filings, but simply surface details.</p> <p><strong>2. Biomedical research agents</strong></p> <p>You ask: "What bioactive compounds in ChEMBL target the KRAS G12C mutation, and how do they relate to current clinical trials?"</p> <p>ChEMBL has 2.5 million bioactive molecule records. ClinicalTrials.gov is partially indexed but the structured data (phase, endpoints, eligibility criteria) isn't extractable via web search.</p> <p><strong>3. Patent landscape analysis</strong></p> <p>You ask: "Which companies hold patents in transformer-based neural architecture search filed after 2022?"</p> <p>USPTO full-text patent search isn't something a lot of Search APIs return well. Some return some data but let you know that it might not be complete or recent.</p> <h2> Deep Research API for Researchers &amp; Non-Developers </h2> <p><a href="proxy.php?url=https://platform.valyu.ai" rel="noopener noreferrer">Valyu</a> has a <strong>"Deep Research"</strong> UI mode simply for non-developers, researchers and folks from all walks of life to have access to the full power of the Deep Research API simply by prompting what you need.</p> <p><a href="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7tpqmojno7uo001z77t.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7tpqmojno7uo001z77t.png" alt="Deep Research for non-developers" width="800" height="480"></a><br> <em>DeepResearch dashboard for the non-coders</em></p> <p>You can see the <strong>Deliverables feature</strong> there as well. Deliverables allow you to extract structured data or create formatted documents (CSV, Excel, PowerPoint, Word, PDF) from the research alongside the report.</p> <h2> Building with Valyu's Deep Research API (for Developers) </h2> <p><a href="proxy.php?url=https://docs.valyu.ai/guides/deepresearch" rel="noopener noreferrer">Valyu's DeepResearch</a> is an async API. You submit a task, it runs in the background, you poll for completion or use webhooks. This is the right architecture for research that takes 30 seconds to 15 minutes depending on complexity.</p> <h3> Installation </h3> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>pip <span class="nb">install </span>valyu <span class="nb">export </span><span class="nv">VALYU_API_KEY</span><span class="o">=</span>your_key_here </code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>pnpm add @valyu/valyu-js <span class="nb">export </span><span class="nv">VALYU_API_KEY</span><span class="o">=</span>your_key_here </code></pre> </div> <h3> Quick Start </h3> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="kn">from</span> <span class="n">valyu</span> <span class="kn">import</span> <span class="n">Valyu</span> <span class="n">valyu</span> <span class="o">=</span> <span class="nc">Valyu</span><span class="p">()</span> <span class="c1"># Create a research task </span><span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">What are the key risk factors disclosed by Nvidia in their 2024 10-K?</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">standard</span><span class="sh">"</span><span class="p">,</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">finance</span><span class="sh">"</span><span class="p">]</span> <span class="c1"># SEC filings, earnings, market data </span> <span class="p">}</span> <span class="p">)</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Task ID: </span><span class="si">{</span><span class="n">task</span><span class="p">.</span><span class="n">deepresearch_id</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Status: </span><span class="si">{</span><span class="n">task</span><span class="p">.</span><span class="n">status</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="c1"># 'running' or 'queued' </span></code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Valyu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/valyu-js</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">valyu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Valyu</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VALYU_API_KEY</span><span class="o">!</span><span class="p">);</span> <span class="c1">// Create a research task</span> <span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">What are the key risk factors disclosed by Nvidia in their 2024 10-K?</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">standard</span><span class="dl">"</span><span class="p">,</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">finance</span><span class="dl">"</span><span class="p">]</span> <span class="c1">// SEC filings, earnings, market data</span> <span class="p">}</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Task ID: </span><span class="p">${</span><span class="nx">task</span><span class="p">.</span><span class="nx">deepresearch_id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Status: </span><span class="p">${</span><span class="nx">task</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="c1">// 'running' or 'queued'</span> </code></pre> </div> <h3> Waiting for Results </h3> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="c1"># Wait for completion with progress tracking </span><span class="k">def</span> <span class="nf">on_progress</span><span class="p">(</span><span class="n">status</span><span class="p">):</span> <span class="k">if</span> <span class="n">status</span><span class="p">.</span><span class="n">progress</span><span class="p">:</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Step </span><span class="si">{</span><span class="n">status</span><span class="p">.</span><span class="n">progress</span><span class="p">.</span><span class="n">current_step</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">status</span><span class="p">.</span><span class="n">progress</span><span class="p">.</span><span class="n">total_steps</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="n">result</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">wait</span><span class="p">(</span> <span class="n">task</span><span class="p">.</span><span class="n">deepresearch_id</span><span class="p">,</span> <span class="n">poll_interval</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">max_wait_time</span><span class="o">=</span><span class="mi">1800</span><span class="p">,</span> <span class="n">on_progress</span><span class="o">=</span><span class="n">on_progress</span> <span class="p">)</span> <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">status</span> <span class="o">==</span> <span class="sh">"</span><span class="s">completed</span><span class="sh">"</span><span class="p">:</span> <span class="nf">print</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">output</span><span class="p">)</span> <span class="c1"># Full markdown report </span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Cost: $</span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">cost</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="k">for</span> <span class="n">source</span> <span class="ow">in</span> <span class="n">result</span><span class="p">.</span><span class="n">sources</span><span class="p">:</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">- </span><span class="si">{</span><span class="n">source</span><span class="p">.</span><span class="n">title</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">source</span><span class="p">.</span><span class="n">url</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> </code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">wait</span><span class="p">(</span><span class="nx">task</span><span class="p">.</span><span class="nx">deepresearch_id</span><span class="p">,</span> <span class="p">{</span> <span class="na">pollInterval</span><span class="p">:</span> <span class="mi">5000</span><span class="p">,</span> <span class="na">maxWaitTime</span><span class="p">:</span> <span class="mi">1800000</span><span class="p">,</span> <span class="na">onProgress</span><span class="p">:</span> <span class="p">(</span><span class="nx">status</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if </span><span class="p">(</span><span class="nx">status</span><span class="p">.</span><span class="nx">progress</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Step </span><span class="p">${</span><span class="nx">status</span><span class="p">.</span><span class="nx">progress</span><span class="p">.</span><span class="nx">currentStep</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">status</span><span class="p">.</span><span class="nx">progress</span><span class="p">.</span><span class="nx">totalSteps</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> <span class="k">if </span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">completed</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">output</span><span class="p">);</span> <span class="c1">// Full markdown report</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Cost: $</span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">cost</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="nx">result</span><span class="p">.</span><span class="nx">sources</span><span class="p">.</span><span class="nf">forEach</span><span class="p">(</span><span class="nx">source</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`- </span><span class="p">${</span><span class="nx">source</span><span class="p">.</span><span class="nx">title</span><span class="p">}</span><span class="s2">: </span><span class="p">${</span><span class="nx">source</span><span class="p">.</span><span class="nx">url</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <h3> Research Modes </h3> <p>Valyu has four modes, optimized for different depth/cost tradeoffs:</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Mode</th> <th>Price</th> <th>Max Steps</th> <th>Best For</th> </tr> </thead> <tbody> <tr> <td><code>fast</code></td> <td>$0.10</td> <td>10</td> <td>Quick lookups, batch processing</td> </tr> <tr> <td><code>standard</code></td> <td>$0.50</td> <td>15</td> <td>Balanced research</td> </tr> <tr> <td><code>heavy</code></td> <td>$2.50</td> <td>15</td> <td>Complex topics, fact verification</td> </tr> <tr> <td><code>max</code></td> <td>$15.00</td> <td>25</td> <td>Exhaustive multi-source analysis</td> </tr> </tbody> </table></div> <p>For most agentic workflows, <code>standard</code> mode hits the right tradeoff. Use <code>fast</code> for high-volume batch tasks. Use <code>heavy</code> or <code>max</code> when you need the agent to cross-verify claims across sources.</p> <h3> Proprietary Source Selection </h3> <p>The <code>search</code> parameter controls which data sources the agent searches.</p> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="c1"># Academic + biomedical research </span><span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Recent advances in CRISPR base editing for sickle cell disease</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">heavy</span><span class="sh">"</span><span class="p">,</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">academic</span><span class="sh">"</span><span class="p">]</span> <span class="c1"># PubMed, arXiv, bioRxiv, medRxiv, clinical trials </span> <span class="p">}</span> <span class="p">)</span> <span class="c1"># Financial + economic analysis </span><span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">How do current FRED interest rate indicators compare to 2008 pre-crisis levels?</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">standard</span><span class="sh">"</span><span class="p">,</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">finance</span><span class="sh">"</span><span class="p">]</span> <span class="c1"># SEC filings, FRED, BLS, stocks, earnings </span> <span class="p">}</span> <span class="p">)</span> <span class="c1"># Patent landscape </span><span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Which companies have filed transformer architecture patents since 2022?</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">heavy</span><span class="sh">"</span><span class="p">,</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">patents</span><span class="sh">"</span><span class="p">]</span> <span class="c1"># USPTO patent database </span> <span class="p">}</span> <span class="p">)</span> <span class="c1"># Cross-domain: web + proprietary combined </span><span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Analyze competitor drug pipeline for NASH treatment</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">max</span><span class="sh">"</span><span class="p">,</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">all</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">academic</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">finance</span><span class="sh">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">)</span> </code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// Academic + biomedical research</span> <span class="kd">const</span> <span class="nx">academicTask</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Recent advances in CRISPR base editing for sickle cell disease</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">heavy</span><span class="dl">"</span><span class="p">,</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">academic</span><span class="dl">"</span><span class="p">]</span> <span class="c1">// PubMed, arXiv, bioRxiv, medRxiv, clinical trials</span> <span class="p">}</span> <span class="p">});</span> <span class="c1">// Financial + economic analysis</span> <span class="kd">const</span> <span class="nx">financeTask</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">How do current FRED interest rate indicators compare to 2008 pre-crisis levels?</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">standard</span><span class="dl">"</span><span class="p">,</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">finance</span><span class="dl">"</span><span class="p">]</span> <span class="c1">// SEC filings, FRED, BLS, stocks, earnings</span> <span class="p">}</span> <span class="p">});</span> <span class="c1">// Patent landscape</span> <span class="kd">const</span> <span class="nx">patentTask</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Which companies have filed transformer architecture patents since 2022?</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">heavy</span><span class="dl">"</span><span class="p">,</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">patents</span><span class="dl">"</span><span class="p">]</span> <span class="c1">// USPTO patent database</span> <span class="p">}</span> <span class="p">});</span> <span class="c1">// Cross-domain: web + proprietary combined</span> <span class="kd">const</span> <span class="nx">crossTask</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Analyze competitor drug pipeline for NASH treatment</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">max</span><span class="dl">"</span><span class="p">,</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">all</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">academic</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">finance</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <p>Available proprietary source categories: <code>academic</code>, <code>finance</code>, <code>patent</code>, <code>legal</code>, <code>transportation</code>, <code>politics</code>.</p> <h3> Structured JSON Output </h3> <p>Instead of markdown, you can define a schema for structured output, useful when your agent needs to pipe results into a database or downstream process.</p> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="n">competitor_schema</span> <span class="o">=</span> <span class="p">{</span> <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">object</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">properties</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span> <span class="sh">"</span><span class="s">companies</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span> <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">array</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">items</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span> <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">object</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">properties</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span> <span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">},</span> <span class="sh">"</span><span class="s">ticker</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">},</span> <span class="sh">"</span><span class="s">key_risk_factors</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span> <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">array</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">items</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">}</span> <span class="p">},</span> <span class="sh">"</span><span class="s">revenue_guidance</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">}</span> <span class="p">},</span> <span class="sh">"</span><span class="s">required</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">key_risk_factors</span><span class="sh">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">},</span> <span class="sh">"</span><span class="s">summary</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">}</span> <span class="p">},</span> <span class="sh">"</span><span class="s">required</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">companies</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">summary</span><span class="sh">"</span><span class="p">]</span> <span class="p">}</span> <span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Extract risk factors and revenue guidance from Q4 2024 10-Ks for major cloud providers</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">heavy</span><span class="sh">"</span><span class="p">,</span> <span class="n">output_formats</span><span class="o">=</span><span class="p">[</span><span class="n">competitor_schema</span><span class="p">],</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">finance</span><span class="sh">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">)</span> </code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">competitorSchema</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">,</span> <span class="na">properties</span><span class="p">:</span> <span class="p">{</span> <span class="na">companies</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">array</span><span class="dl">"</span><span class="p">,</span> <span class="na">items</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">,</span> <span class="na">properties</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span> <span class="na">ticker</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span> <span class="na">key_risk_factors</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">array</span><span class="dl">"</span><span class="p">,</span> <span class="na">items</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">}</span> <span class="p">},</span> <span class="na">revenue_guidance</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">}</span> <span class="p">},</span> <span class="na">required</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">key_risk_factors</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">},</span> <span class="na">summary</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">}</span> <span class="p">},</span> <span class="na">required</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">companies</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">summary</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span> <span class="k">as</span> <span class="kd">const</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Extract risk factors and revenue guidance from Q4 2024 10-Ks for major cloud providers</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">heavy</span><span class="dl">"</span><span class="p">,</span> <span class="na">output_formats</span><span class="p">:</span> <span class="p">[</span><span class="nx">competitorSchema</span><span class="p">],</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">finance</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <p>The API returns JSON that conforms to your schema, ready to deserialize directly.</p> <h3> Webhooks for Production </h3> <p>Don't poll in production. Use webhooks.</p> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="kn">import</span> <span class="n">hmac</span> <span class="kn">import</span> <span class="n">hashlib</span> <span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">jsonify</span> <span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span> <span class="n">WEBHOOK_SECRET</span> <span class="o">=</span> <span class="sh">"</span><span class="s">your-stored-secret</span><span class="sh">"</span> <span class="c1"># returned on task creation, store it </span> <span class="nd">@app.route</span><span class="p">(</span><span class="sh">"</span><span class="s">/webhooks/deepresearch</span><span class="sh">"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">POST</span><span class="sh">"</span><span class="p">])</span> <span class="k">def</span> <span class="nf">handle_research_complete</span><span class="p">():</span> <span class="n">signature</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">headers</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">X-Webhook-Signature</span><span class="sh">"</span><span class="p">)</span> <span class="n">timestamp</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">headers</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">X-Webhook-Timestamp</span><span class="sh">"</span><span class="p">)</span> <span class="n">payload</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">get_data</span><span class="p">(</span><span class="n">as_text</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">signed</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">timestamp</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">payload</span><span class="si">}</span><span class="sh">"</span> <span class="n">expected</span> <span class="o">=</span> <span class="sh">"</span><span class="s">sha256=</span><span class="sh">"</span> <span class="o">+</span> <span class="n">hmac</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span> <span class="n">WEBHOOK_SECRET</span><span class="p">.</span><span class="nf">encode</span><span class="p">(),</span> <span class="n">signed</span><span class="p">.</span><span class="nf">encode</span><span class="p">(),</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha256</span> <span class="p">).</span><span class="nf">hexdigest</span><span class="p">()</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">hmac</span><span class="p">.</span><span class="nf">compare_digest</span><span class="p">(</span><span class="n">expected</span><span class="p">,</span> <span class="n">signature</span><span class="p">):</span> <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Invalid signature</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">401</span> <span class="n">data</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">json</span> <span class="k">if</span> <span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">status</span><span class="sh">"</span><span class="p">]</span> <span class="o">==</span> <span class="sh">"</span><span class="s">completed</span><span class="sh">"</span><span class="p">:</span> <span class="nf">process_research_result</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">deepresearch_id</span><span class="sh">"</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">output</span><span class="sh">"</span><span class="p">])</span> <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">received</span><span class="sh">"</span><span class="p">:</span> <span class="bp">True</span><span class="p">}),</span> <span class="mi">200</span> <span class="c1"># Task creation with webhook </span><span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Comprehensive competitive analysis: SEC filings + market data</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">max</span><span class="sh">"</span><span class="p">,</span> <span class="n">webhook_url</span><span class="o">=</span><span class="sh">"</span><span class="s">https://your-app.com/webhooks/deepresearch</span><span class="sh">"</span> <span class="p">)</span> <span class="c1"># CRITICAL: store task.webhook_secret immediately - only returned once </span><span class="nf">store_secret</span><span class="p">(</span><span class="n">task</span><span class="p">.</span><span class="n">deepresearch_id</span><span class="p">,</span> <span class="n">task</span><span class="p">.</span><span class="n">webhook_secret</span><span class="p">)</span> </code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="nx">crypto</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">crypto</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">express</span><span class="p">,</span> <span class="p">{</span> <span class="nx">Request</span><span class="p">,</span> <span class="nx">Response</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">express</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nf">express</span><span class="p">();</span> <span class="nx">app</span><span class="p">.</span><span class="nf">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nf">raw</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span> <span class="p">}));</span> <span class="kd">const</span> <span class="nx">WEBHOOK_SECRET</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">WEBHOOK_SECRET</span><span class="o">!</span><span class="p">;</span> <span class="nx">app</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="dl">"</span><span class="s2">/webhooks/deepresearch</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">:</span> <span class="nx">Request</span><span class="p">,</span> <span class="nx">res</span><span class="p">:</span> <span class="nx">Response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">signature</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">x-webhook-signature</span><span class="dl">"</span><span class="p">]</span> <span class="k">as</span> <span class="kr">string</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">timestamp</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">x-webhook-timestamp</span><span class="dl">"</span><span class="p">]</span> <span class="k">as</span> <span class="kr">string</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nf">toString</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">signed</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">timestamp</span><span class="p">}</span><span class="s2">.</span><span class="p">${</span><span class="nx">payload</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">expected</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">sha256=</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">crypto</span> <span class="p">.</span><span class="nf">createHmac</span><span class="p">(</span><span class="dl">"</span><span class="s2">sha256</span><span class="dl">"</span><span class="p">,</span> <span class="nx">WEBHOOK_SECRET</span><span class="p">)</span> <span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="nx">signed</span><span class="p">)</span> <span class="p">.</span><span class="nf">digest</span><span class="p">(</span><span class="dl">"</span><span class="s2">hex</span><span class="dl">"</span><span class="p">);</span> <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">crypto</span><span class="p">.</span><span class="nf">timingSafeEqual</span><span class="p">(</span><span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">expected</span><span class="p">),</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">signature</span><span class="p">)))</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">status</span><span class="p">(</span><span class="mi">401</span><span class="p">).</span><span class="nf">json</span><span class="p">({</span> <span class="na">error</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Invalid signature</span><span class="dl">"</span> <span class="p">});</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span> <span class="k">if </span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">completed</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="nf">processResearchResult</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">deepresearch_id</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">output</span><span class="p">);</span> <span class="p">}</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">({</span> <span class="na">received</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span> <span class="p">});</span> <span class="c1">// Task creation with webhook</span> <span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Comprehensive competitive analysis: SEC filings + market data</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">max</span><span class="dl">"</span><span class="p">,</span> <span class="na">webhook_url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://your-app.com/webhooks/deepresearch</span><span class="dl">"</span> <span class="p">});</span> <span class="c1">// CRITICAL: store task.webhookSecret immediately - only returned once</span> <span class="k">await</span> <span class="nf">storeSecret</span><span class="p">(</span><span class="nx">task</span><span class="p">.</span><span class="nx">deepresearch_id</span><span class="p">,</span> <span class="nx">task</span><span class="p">.</span><span class="nx">webhook_secret</span><span class="p">);</span> </code></pre> </div> <h3> Date Filtering </h3> <p>For time-sensitive research tasks, you can pin the search window.</p> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Clinical trial results for GLP-1 receptor agonists in NASH</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">standard</span><span class="sh">"</span><span class="p">,</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">academic</span><span class="sh">"</span><span class="p">],</span> <span class="sh">"</span><span class="s">start_date</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">2023-01-01</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">end_date</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">2025-12-31</span><span class="sh">"</span> <span class="p">}</span> <span class="p">)</span> </code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Clinical trial results for GLP-1 receptor agonists in NASH</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">standard</span><span class="dl">"</span><span class="p">,</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">academic</span><span class="dl">"</span><span class="p">],</span> <span class="na">start_date</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2023-01-01</span><span class="dl">"</span><span class="p">,</span> <span class="na">end_date</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2025-12-31</span><span class="dl">"</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <p>This prevents the agent from pulling in outdated studies. This is very important for medical or financial research where recency matters.</p> <h3> Attach Documents for Analysis &amp; Inclusion </h3> <p>Feed existing documents of all types into the research task. This is useful for combining internal documents with external research.</p> <p><strong>Python</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="kn">import</span> <span class="n">base64</span> <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="sh">"</span><span class="s">internal_q4_report.pdf</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">rb</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span> <span class="n">pdf_b64</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="nf">b64encode</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">()).</span><span class="nf">decode</span><span class="p">()</span> <span class="n">task</span> <span class="o">=</span> <span class="n">valyu</span><span class="p">.</span><span class="n">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">Compare the Q4 projections in this internal report against actual SEC filing data for our competitors</span><span class="sh">"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="sh">"</span><span class="s">heavy</span><span class="sh">"</span><span class="p">,</span> <span class="n">files</span><span class="o">=</span><span class="p">[{</span> <span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">data:application/pdf;base64,</span><span class="si">{</span><span class="n">pdf_b64</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">filename</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">q4_report.pdf</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">mediaType</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">application/pdf</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">context</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Internal Q4 2024 financial projections</span><span class="sh">"</span> <span class="p">}],</span> <span class="n">search</span><span class="o">=</span><span class="p">{</span> <span class="sh">"</span><span class="s">search_type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">proprietary</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">included_sources</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">finance</span><span class="sh">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">)</span> </code></pre> </div> <p><strong>TypeScript</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">fs</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">pdfBuffer</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nf">readFileSync</span><span class="p">(</span><span class="dl">"</span><span class="s2">internal_q4_report.pdf</span><span class="dl">"</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">pdfB64</span> <span class="o">=</span> <span class="nx">pdfBuffer</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="dl">"</span><span class="s2">base64</span><span class="dl">"</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nx">deepresearch</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Compare the Q4 projections in this internal report against actual SEC filing data for our competitors</span><span class="dl">"</span><span class="p">,</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">heavy</span><span class="dl">"</span><span class="p">,</span> <span class="na">files</span><span class="p">:</span> <span class="p">[{</span> <span class="na">data</span><span class="p">:</span> <span class="s2">`data:application/pdf;base64,</span><span class="p">${</span><span class="nx">pdfB64</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="na">filename</span><span class="p">:</span> <span class="dl">"</span><span class="s2">q4_report.pdf</span><span class="dl">"</span><span class="p">,</span> <span class="na">mediaType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/pdf</span><span class="dl">"</span><span class="p">,</span> <span class="na">context</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Internal Q4 2024 financial projections</span><span class="dl">"</span> <span class="p">}],</span> <span class="na">search</span><span class="p">:</span> <span class="p">{</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">finance</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2> Deep Research API in Production </h2> <p>"Talk is cheap, show me the code, and show it to me in production" - Odogwu Machalla</p> <p><a href="proxy.php?url=https://consultralph.com" rel="noopener noreferrer">Consult Ralph</a> is an AI-powered Deep Research app for consultants. It's in production, currently used by hundreds of consultants daily. The app is heavily powered by <a href="proxy.php?url=https://www.valyu.ai/solutions-research" rel="noopener noreferrer">Valyu DeepResearch API.</a></p> <p><iframe class="tweet-embed" id="tweet-2020959881935962295-719" src="proxy.php?url=https://platform.twitter.com/embed/Tweet.html?id=2020959881935962295"> </iframe> // Detect dark theme var iframe = document.getElementById('tweet-2020959881935962295-719'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2020959881935962295&amp;theme=dark" } </p> <p>It's also <a href="proxy.php?url=https://github.com/unicodeveloper/consultralph" rel="noopener noreferrer">open-source</a>. You can fork, clone, star it and check out the code for good references on how to use the Valyu Deep Research API in your codebase.</p> <h2> Benchmarks </h2> <p><strong>Valyu's DeepResearch</strong> scores <strong>53.1 on DeepResearch-Bench</strong>. The best published score for any commercial deep research API.</p> <p>On domain-specific benchmarks where proprietary data access matters:</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Benchmark</th> <th>Valyu</th> <th>Parallel</th> <th>Exa</th> <th>Google</th> </tr> </thead> <tbody> <tr> <td>Finance (120 questions)</td> <td><strong>73%</strong></td> <td>67%</td> <td>63%</td> <td>55%</td> </tr> <tr> <td>Economics (100 questions)</td> <td><strong>73%</strong></td> <td>52%</td> <td>45%</td> <td>43%</td> </tr> <tr> <td>MedAgent (562 medical queries)</td> <td><strong>48%</strong></td> <td>42%</td> <td>44%</td> <td>45%</td> </tr> <tr> <td>FreshQA (600 time-sensitive)</td> <td><strong>79%</strong></td> <td>52%</td> <td>24%</td> <td>39%</td> </tr> </tbody> </table></div> <p>The finance and economics gaps are almost entirely explained by proprietary data access. Valyu queries FRED, BLS, and SEC directly. Web-only APIs rely on articles that reference this data, which is noisier and often outdated.</p> <h2> Frequently Asked Questions </h2> <p><strong>What is a deep research API?</strong></p> <p>A deep research API is a programmatic interface that performs multi-step autonomous research. It accepts a query, plans a research strategy, searches multiple sources, synthesizes results, and returns a structured report with citations. Unlike a standard search API, deep research APIs run asynchronously - tasks can take seconds to minutes depending on depth.</p> <p><strong>How does Valyu's DeepResearch API differ from OpenAI's?</strong></p> <p>OpenAI's deep research (o3-deep-research, o4-mini-deep-research) only searches the web via Bing. Valyu's DeepResearch searches both the web and 36+ proprietary data sources including SEC filings, PubMed, arXiv, clinical trials, USPTO patents, FRED economic data, and ChEMBL bioactive compounds. For AI agents that need authoritative domain data rather than general web content, this is the meaningful difference.</p> <p><strong>What does the Valyu DeepResearch API cost?</strong></p> <p>Four modes: fast ($0.10/task), standard ($0.50/task), heavy ($2.50/task), max ($15.00/task). Pricing is per task regardless of the number of searches or retrievals the agent performs internally. Signup gets $10 in free credits, no card required.</p> <p><strong>Does Valyu's deep research API support webhooks?</strong></p> <p>Yes. Pass a <code>webhook_url</code> on task creation. The API returns a <code>webhook_secret</code> for signature verification. Webhooks fire on task completion or failure with the full output payload. Retries use exponential backoff (up to 5 attempts) on server errors.</p> <p><strong>Can I get structured JSON output from a deep research API?</strong></p> <p>Valyu supports custom JSON Schema for structured output. Pass a schema object in <code>output_formats</code>. The API returns output that conforms to your schema, ready to deserialize directly into your application's data structures.</p> <p><strong>How long does a deep research task take?</strong></p> <p>Fast mode typically completes in under 60 seconds. Standard mode takes 2-5 minutes. Heavy and max modes can take up to 15-30 minutes for complex multi-source analysis. Use the <code>wait()</code> method with progress callbacks for synchronous use cases, or webhooks for production event-driven workflows.</p> <p><strong>Can I attach documents to a deep research task?</strong></p> <p>Yes. Pass files as base64-encoded data with a media type. The API supports PDFs, images (PNG, JPEG, WebP), and other documents. Useful for combining internal documents with external research - for example, analyzing your internal financial projections against publicly filed SEC data.</p> <p><strong>Is there a TypeScript / JavaScript SDK?</strong></p> <p>Yes. <code>pnpm add @valyu/valyu-js</code>. The API surface is identical to the Python SDK - all examples in this guide are shown in both languages.</p> <h2> Summary </h2> <p>If you're building AI agents that only research general web content, OpenAI or Perplexity deep research are solid choices, including Valyu. If your agents need to touch SEC filings, academic papers behind paywalls, clinical trial databases, USPTO patents, or real financial data, you need an API with proprietary source access.</p> <p>Valyu's DeepResearch API:</p> <ul> <li>Four modes from $0.10 (fast) to $15.00 (max)</li> <li>36+ proprietary data sources across finance, biomedical, academic, patent, and legal domains</li> <li>Markdown, PDF, and structured JSON output</li> <li>Webhook and polling support</li> <li>53.1 on DeepResearch-Bench (best published commercial score)</li> </ul> <p>Docs at <a href="proxy.php?url=https://docs.valyu.ai/guides/deepresearch" rel="noopener noreferrer">docs.valyu.ai/guides/deepresearch</a>. </p> <p>$10 free credits on signup at <a href="proxy.php?url=https://platform.valyu.ai" rel="noopener noreferrer">platform.valyu.ai</a>.</p> deepresearch ai python typescript Why your AI agent keeps hallucinating financial data (and how to fix it) Prosper Otemuyiwa Fri, 27 Feb 2026 12:02:18 +0000 https://dev.to/valyuai/why-your-ai-agent-keeps-hallucinating-financial-data-and-how-to-fix-it-180d https://dev.to/valyuai/why-your-ai-agent-keeps-hallucinating-financial-data-and-how-to-fix-it-180d <p>You asked your financial agent for NVIDIA's current P/E ratio. It answered: 40.2.</p> <p>The actual number was 45.65.</p> <p>You asked it to summarize the key risks from a company's latest 10-K. It cited concerns that were quietly removed two annual reports ago.</p> <p>You asked for Apple's most recent quarterly revenue. Off by $3 billion.</p> <p>This is not a hallucination problem in the sense you might think. The LLM isn't randomly generating numbers. It's retrieving the most statistically likely answer from its training data, and doing it confidently. The problem is that financial data has a shelf life measured in hours, sometimes minutes and LLM training data has a shelf life measured in years or months.</p> <p>This is a data access problem, not an intelligence problem. And it has a clean fix.</p> <h2> Why the training cutoff ruins financial agents </h2> <p>GPT-5.2's training data cuts off is <strong>August 31, 2025</strong>. Claude 4.6 Sonnet's is <strong>August 2025</strong>.</p> <p>Stock prices move by the second. Earnings drop quarterly. The Fed makes a rate decision and markets reprice overnight. A company files an 8-K about a material event and that changes everything. LLMs have none of this.</p> <p>What makes it worse is that the model doesn't know it's wrong. When you ask for <strong>Microsoft's current P/E ratio</strong>, it has an answer. That answer was accurate at some point during training. It delivers it with the same confidence as if it just pulled the number off a live exchange. No hedging, no "as of my knowledge cutoff" qualifier, unless you've explicitly prompted for it, and even then it often still gives you a number.</p> <p><strong>The result:</strong> An agent that sounds authoritative while being factually wrong on every time-sensitive financial data point.</p> <p>For general Q&amp;A this is acceptable. For anything financial, it's a liability.</p> <h2> The two approaches that don't actually work </h2> <h3> Approach 1: Prompt the model harder </h3> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">generateText</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">openai</span><span class="p">(</span><span class="dl">'</span><span class="s1">gpt-5.2</span><span class="dl">'</span><span class="p">),</span> <span class="na">prompt</span><span class="p">:</span> <span class="s2">`You are a financial expert. Always provide accurate, up-to-date financial data. Today's date is </span><span class="p">${</span><span class="k">new</span> <span class="nc">Date</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">()}</span><span class="s2">. What is Apple's current stock price?`</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <p>This does nothing useful. Telling the model today's date doesn't give it access to today's data. It still answers from training data. Worse, the explicit date sometimes triggers more confident wrong answers because the model pattern-matches "I know this domain" and generates a plausible-sounding number.<br> <br></p> <h3> Approach 2: RAG with financial documents </h3> <p>Some teams build a RAG pipeline: scrape financial reports, chunk them, embed them, retrieve on query. This is better than nothing but it creates a new set of problems:</p> <ul> <li>You're now responsible for keeping the document store current</li> <li>Scraped financial documents lose structure (tables, footnotes, cross-references)</li> <li>Your retrieval quality determines your answer quality</li> <li>SEC filings alone average 40,000 words. Chunking strategies matter enormously</li> <li>You're essentially rebuilding a financial data API, badly</li> </ul> <p>The root problem isn’t your retrieval strategy. It’s that you’re trying to solve a live data problem with a static data architecture.</p> <p>Let’s be honest. It’s 2026. Why are you still building and maintaining your own RAG pipeline from scratch?</p> <p>Vector DB tuning. Chunking debates. Re-indexing jobs. Infra bills creeping up every month. Edge cases multiplying. It gets expensive fast. And the maintenance burden compounds even faster.</p> <h2> The actual fix: live data as tools </h2> <p>The correct mental model is this: <strong>An LLM should reason over financial data, not store it</strong>.</p> <p>The LLM is good at understanding context, synthesizing information, drawing conclusions, and communicating clearly. It's bad at being a database. Stop asking it to be one.</p> <p>The fix is to give your agent tools that query live financial data at inference time. When the agent needs a stock price, it calls a tool and gets the current price. When it needs SEC filings, it searches them in real time. The LLM never touches a stale number.</p> <p>Here's what this looks like with the <strong>Vercel AI SDK</strong> and <strong>TypeScript</strong>:</p> <h3> Basic setup </h3> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>pnpm add @valyu/ai-sdk ai @ai-sdk/openai </code></pre> </div> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">generateText</span><span class="p">,</span> <span class="nx">stepCountIs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">financeSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/ai-sdk</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">openai</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@ai-sdk/openai</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">text</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">generateText</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">openai</span><span class="p">(</span><span class="dl">'</span><span class="s1">gpt-5.2</span><span class="dl">'</span><span class="p">),</span> <span class="na">prompt</span><span class="p">:</span> <span class="dl">'</span><span class="s1">What is the current P/E ratio for NVIDIA and how does it compare to AMD?</span><span class="dl">'</span><span class="p">,</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">financeSearch</span><span class="p">:</span> <span class="nf">financeSearch</span><span class="p">(),</span> <span class="p">},</span> <span class="na">stopWhen</span><span class="p">:</span> <span class="nf">stepCountIs</span><span class="p">(</span><span class="mi">5</span><span class="p">),</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span> </code></pre> </div> <p>When the agent runs this, it doesn't guess. It calls <code>financeSearch</code> with a query, gets back current market data, and reasons over it. The number it tells you is the number that was retrieved from a live source at the moment you asked.</p> <h3> Building a financial research agent </h3> <p>Here's a more complete example, a streaming financial agent you can drop into a Next.js API route:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// app/api/finance/route.ts</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">openai</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ai-sdk/openai</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">financeSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@valyu/ai-sdk</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">streamText</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">ai</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">POST</span><span class="p">(</span><span class="nx">req</span><span class="p">:</span> <span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">messages</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">req</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">streamText</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">openai</span><span class="p">(</span><span class="dl">'</span><span class="s1">gpt-5.2</span><span class="dl">'</span><span class="p">),</span> <span class="nx">messages</span><span class="p">,</span> <span class="na">system</span><span class="p">:</span> <span class="s2">`You are a financial analyst with access to real-time market data and SEC filings. When asked about any financial metric, stock price, earnings figure, or company filing, always use your search tool to retrieve current data. Never rely on prior knowledge for financial figures. Cite your sources.`</span><span class="p">,</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">searchFinance</span><span class="p">:</span> <span class="nf">financeSearch</span><span class="p">(),</span> <span class="p">},</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">result</span><span class="p">.</span><span class="nf">toDataStreamResponse</span><span class="p">();</span> <span class="p">}</span> </code></pre> </div> <p>The critical part is the system prompt instruction: <em>"Always use your search tool to retrieve current data. Never rely on prior knowledge for financial figures."</em></p> <p>This forces the agent to go live on every financial query instead of falling back to training data.</p> <h2> Going deeper: SEC filings and Earnings data </h2> <p>Stock prices are the obvious case. But the same problem applies to everything structural: balance sheets, income statements, risk factors, insider transactions, earnings guidance.</p> <p>Here's how to search SEC filings specifically:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Valyu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">valyu-js</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">valyu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Valyu</span><span class="p">();</span> <span class="c1">// Search for specific disclosure language across recent 10-K filings</span> <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">valyu</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span> <span class="dl">"</span><span class="s2">material risk factors related to AI compute supply chain 2024</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">searchType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">includedSources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">valyu/valyu-sec-filings</span><span class="dl">"</span><span class="p">],</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">large</span><span class="dl">"</span><span class="p">,</span> <span class="p">}</span> <span class="p">);</span> <span class="nx">response</span><span class="p">.</span><span class="nx">results</span><span class="p">.</span><span class="nf">forEach</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Filing: </span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">title</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Source: </span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">url</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`Excerpt: </span><span class="p">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">400</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span> <span class="p">});</span> </code></pre> </div> <p>This returns actual filing content. Not summaries, not news articles about filings, the actual text from 10-Ks. </p> <p>Natural language queries work. You don't need ticker symbols or accession numbers.</p> <p>For combining market data with fundamentals in one agent:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">generateText</span><span class="p">,</span> <span class="nx">stepCountIs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">financeSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/ai-sdk</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">openai</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@ai-sdk/openai</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">text</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">generateText</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">openai</span><span class="p">(</span><span class="dl">'</span><span class="s1">gpt-5.2</span><span class="dl">'</span><span class="p">),</span> <span class="na">prompt</span><span class="p">:</span> <span class="s2">`Analyze Microsoft's financial position: current valuation multiples, most recent quarterly earnings vs expectations, and any material risk disclosures from their latest 10-K.`</span><span class="p">,</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">finance</span><span class="p">:</span> <span class="nf">financeSearch</span><span class="p">(),</span> <span class="p">},</span> <span class="na">stopWhen</span><span class="p">:</span> <span class="nf">stepCountIs</span><span class="p">(</span><span class="mi">10</span><span class="p">),</span> <span class="p">});</span> </code></pre> </div> <p>The agent will make multiple tool calls. One for market data, one for earnings, one for SEC filings and synthesize the results into a coherent analysis. All from live sources.</p> <h2> Combining multiple financial data sources </h2> <p>The most useful financial agents cross-reference data types. An earnings miss is more meaningful when you can see it alongside the stock reaction, the analyst revision history, and what management said in the 8-K. Here's a multi-source pattern:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Valyu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">valyu-js</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">valyu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Valyu</span><span class="p">();</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">analyzeCompany</span><span class="p">(</span><span class="nx">ticker</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Parallel queries across data types</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">marketData</span><span class="p">,</span> <span class="nx">fundamentals</span><span class="p">,</span> <span class="nx">filings</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nf">all</span><span class="p">([</span> <span class="nx">valyu</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">ticker</span><span class="p">}</span><span class="s2"> stock price market cap volume`</span><span class="p">,</span> <span class="p">{</span> <span class="na">searchType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">includedSources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">valyu/valyu-stocks</span><span class="dl">"</span><span class="p">],</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="p">}),</span> <span class="nx">valyu</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">ticker</span><span class="p">}</span><span class="s2"> earnings revenue EPS most recent quarter`</span><span class="p">,</span> <span class="p">{</span> <span class="na">searchType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">includedSources</span><span class="p">:</span> <span class="p">[</span> <span class="dl">"</span><span class="s2">valyu/valyu-earnings-US</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">valyu/valyu-income-statement-US</span><span class="dl">"</span><span class="p">,</span> <span class="p">],</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="p">}),</span> <span class="nx">valyu</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">ticker</span><span class="p">}</span><span class="s2"> 10-K risk factors material disclosures`</span><span class="p">,</span> <span class="p">{</span> <span class="na">searchType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">includedSources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">valyu/valyu-sec-filings</span><span class="dl">"</span><span class="p">],</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="na">responseLength</span><span class="p">:</span> <span class="dl">"</span><span class="s2">large</span><span class="dl">"</span><span class="p">,</span> <span class="p">}),</span> <span class="p">]);</span> <span class="k">return</span> <span class="p">{</span> <span class="na">market</span><span class="p">:</span> <span class="nx">marketData</span><span class="p">.</span><span class="nx">results</span><span class="p">,</span> <span class="na">fundamentals</span><span class="p">:</span> <span class="nx">fundamentals</span><span class="p">.</span><span class="nx">results</span><span class="p">,</span> <span class="na">filings</span><span class="p">:</span> <span class="nx">filings</span><span class="p">.</span><span class="nx">results</span><span class="p">,</span> <span class="p">};</span> <span class="p">}</span> </code></pre> </div> <p>This is the pattern that makes financial agents actually useful. You're not asking the LLM to recall financial data, you're giving it three live data streams to reason over.</p> <h2> Handling the response in a streaming UI </h2> <p>If you want this in a chat interface, the Vercel AI SDK handles the streaming side cleanly:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// app/page.tsx</span> <span class="dl">'</span><span class="s1">use client</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useChat</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">ai/react</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">FinanceAgent</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">messages</span><span class="p">,</span> <span class="nx">input</span><span class="p">,</span> <span class="nx">handleInputChange</span><span class="p">,</span> <span class="nx">handleSubmit</span><span class="p">,</span> <span class="nx">isLoading</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">useChat</span><span class="p">({</span> <span class="na">api</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/api/finance</span><span class="dl">'</span><span class="p">,</span> <span class="p">});</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex flex-col h-screen max-w-3xl mx-auto p-4</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex-1 overflow-y-auto space-y-4 mb-4</span><span class="dl">"</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">messages</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">message</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">message</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`flex </span><span class="p">${</span> <span class="nx">message</span><span class="p">.</span><span class="nx">role</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">justify-end</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">justify-start</span><span class="dl">'</span> <span class="p">}</span><span class="s2">`</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`rounded-lg px-4 py-2 max-w-2xl </span><span class="p">${</span> <span class="nx">message</span><span class="p">.</span><span class="nx">role</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">bg-blue-500 text-white</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">bg-gray-100 text-gray-900</span><span class="dl">'</span> <span class="p">}</span><span class="s2">`</span><span class="p">}</span> <span class="o">&gt;</span> <span class="p">{</span><span class="nx">message</span><span class="p">.</span><span class="nx">content</span><span class="p">}</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">))}</span> <span class="p">{</span><span class="nx">isLoading</span> <span class="o">&amp;&amp;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-gray-400 text-sm</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Searching</span> <span class="nx">live</span> <span class="nx">data</span><span class="p">...</span><span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)}</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">form</span> <span class="nx">onSubmit</span><span class="o">=</span><span class="p">{</span><span class="nx">handleSubmit</span><span class="p">}</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex gap-2</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">input</span> <span class="nx">value</span><span class="o">=</span><span class="p">{</span><span class="nx">input</span><span class="p">}</span> <span class="nx">onChange</span><span class="o">=</span><span class="p">{</span><span class="nx">handleInputChange</span><span class="p">}</span> <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">Ask about any company or market...</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex-1 px-4 py-2 border rounded-lg</span><span class="dl">"</span> <span class="nx">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">isLoading</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">button</span> <span class="kd">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">submit</span><span class="dl">"</span> <span class="nx">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">isLoading</span><span class="p">}</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">px-6 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="p">{</span><span class="nx">isLoading</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">Searching...</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">Ask</span><span class="dl">'</span><span class="p">}</span> <span class="o">&lt;</span><span class="sr">/button</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/form</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">}</span> </code></pre> </div> <h2> What changes once you do this </h2> <p>The quantitative difference is real. On a benchmark of 120 finance-specific questions, agents using live proprietary data access score 73% accuracy. Agents using GPT with no tool access score significantly lower, and every miss is a confident, plausible-sounding wrong answer.</p> <p>A more practical scenario: When a user asks your agent about a company's debt-to-equity ratio, they're probably making a decision. The cost of a confidently wrong answer is not a user complaint, it's a user making a bad decision because your tool told them something false with authority.</p> <p>The architecture shift here is small. You're adding a tool definition and changing a system prompt instruction. The result is an agent that reasons accurately over live financial data instead of pattern-matching from stale training.</p> <h2> FAQ </h2> <p><strong>Q: Does this work with Claude and other models, not just OpenAI?</strong></p> <p>Yes. The Vercel AI SDK supports any model through their provider system. Swap <code>openai('gpt-5.2')</code> for <code>anthropic('claude-sonnet-4-6')</code> and the tool calling works identically. Financial data tools are model-agnostic.</p> <p><strong>Q: What financial data sources does this actually cover?</strong></p> <p>The <code>financeSearch</code> tool routes across stocks (200K+), crypto (200+ coins), forex (180+ pairs), ETFs (25K+), plus company fundamentals, earnings, balance sheets, income statements, cash flows, dividends, insider transactions. And SEC filings (3M+ documents: 10-K, 10-Q, 8-K, Form 4s). All queryable via natural language.</p> <p><strong>Q: What about FRED economic data and macro indicators?</strong></p> <p>You can include <code>valyu/valyu-fred</code> and <code>valyu/valyu-bls</code> as sources in the search. Same pattern. Just pass them in <code>includedSources</code> and query in natural language. Useful for macro context alongside company-level data.</p> <p><strong>Q: How do I avoid the agent making too many tool calls and running up costs?</strong></p> <p><code>stopWhen: stepCountIs(N)</code> from the Vercel AI SDK controls the maximum number of tool call steps. For simple queries, <code>stepCountIs(3)</code> is usually enough. For deep research queries, <code>stepCountIs(10)</code> gives more room. You can also set <code>maxPrice</code> on the search tool itself to cap per-result retrieval costs.</p> <p><strong>Q: Should I still use RAG for financial data?</strong></p> <p>RAG makes sense for private documents you own. Internal research reports, your own financial models, proprietary analysis. For public financial data (market prices, SEC filings, earnings) that changes continuously, live API access is more appropriate. Don't use static RAG for data that has a freshness requirement.</p> <p><strong>Q: Does this work for real-time stock prices or just historical data?</strong></p> <p>Both. Market data has a 1-5 minute delay on real-time prices. Historical data is available going back years. For most analytical use cases, the 1-5 minute delay is irrelevant. If you're building a trading system that requires millisecond precision, you need a dedicated market data feed. For financial research agents, this is more than sufficient.</p> <p>The code in this article is runnable as-is with a <code>VALYU_API_KEY</code> and <code>OPENAI_API_KEY</code> in your environment. </p> <p>Full working examples are in the <a href="proxy.php?url=https://github.com/valyuAI/cookbook" rel="noopener noreferrer">Valyu cookbook on GitHub</a>.</p> <p>Here's a real-world Finance AI agent - <a href="proxy.php?url=https://finance.valyu.ai" rel="noopener noreferrer">https://finance.valyu.ai</a>. The repo is <a href="proxy.php?url=https://github.com/yorkeccak/finance" rel="noopener noreferrer">open-source</a></p> <p>If you're building something with this, drop it in the comments. I'm curious what use cases people are working on.</p> typescript ai webdev agents How I Built a Live Stock Market AI Agent in ~40 Lines of TypeScript Prosper Otemuyiwa Tue, 24 Feb 2026 11:48:37 +0000 https://dev.to/valyuai/how-i-built-a-live-stock-market-ai-agent-in-40-lines-of-typescript-3aa3 https://dev.to/valyuai/how-i-built-a-live-stock-market-ai-agent-in-40-lines-of-typescript-3aa3 <p>I got tired of jumping between Yahoo Finance, a broker dashboard, and a Google News tab every time I wanted a quick read on a stock. What I actually wanted: type a question, get a grounded answer that pulls live prices, recent earnings, and current news in one shot.</p> <p>Turns out you can wire this up in an afternoon with Vercel AI SDK and Valyu's <code>financeSearch</code> + <code>webSearch</code> tools. Here's how.</p> <h2> What we're building </h2> <p>A Node.js CLI agent you run like this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npx tsx agent.ts <span class="s2">"What's happening with NVDA? Give me price, recent earnings, and any news I should know about."</span> </code></pre> </div> <p>It streams back a real answer, not a hallucinated one, pulled from live financial data and current web sources.</p> <h2> Setup </h2> <p>You need two things:</p> <ol> <li>A Valyu API key - free $10 credit when you sign up at <a href="proxy.php?url=https://platform.valyu.ai" rel="noopener noreferrer">platform.valyu.ai</a> </li> <li>An Anthropic API key (or swap in OpenAI/any other AI SDK provider) </li> </ol> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>pnpm add ai @ai-sdk/anthropic @valyu/ai-sdk </code></pre> </div> <p>Create a <code>.env</code> file:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>VALYU_API_KEY=your-valyu-key ANTHROPIC_API_KEY=your-anthropic-key </code></pre> </div> <h2> The agent </h2> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// agent.ts</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">streamText</span><span class="p">,</span> <span class="nx">stepCountIs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">anthropic</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@ai-sdk/anthropic</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">financeSearch</span><span class="p">,</span> <span class="nx">webSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/ai-sdk</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">What's the current price and outlook for AAPL?</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">streamText</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="nf">anthropic</span><span class="p">(</span><span class="dl">"</span><span class="s2">claude-3-5-sonnet-20241022</span><span class="dl">"</span><span class="p">),</span> <span class="na">messages</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">role</span><span class="p">:</span> <span class="dl">"</span><span class="s2">system</span><span class="dl">"</span><span class="p">,</span> <span class="na">content</span><span class="p">:</span> <span class="s2">`You are a stock market analyst with access to live financial data and web search. - Use financeSearch for stock prices, earnings, dividends, balance sheets, and insider transactions - Use webSearch for recent news, analyst sentiment, and market context - Always cite sources - Be specific: include actual numbers, dates, and figures from your search results`</span><span class="p">,</span> <span class="p">},</span> <span class="p">{</span> <span class="na">role</span><span class="p">:</span> <span class="dl">"</span><span class="s2">user</span><span class="dl">"</span><span class="p">,</span> <span class="na">content</span><span class="p">:</span> <span class="nx">query</span><span class="p">,</span> <span class="p">},</span> <span class="p">],</span> <span class="na">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">financeSearch</span><span class="p">:</span> <span class="nf">financeSearch</span><span class="p">(),</span> <span class="na">webSearch</span><span class="p">:</span> <span class="nf">webSearch</span><span class="p">(),</span> <span class="p">},</span> <span class="na">stopWhen</span><span class="p">:</span> <span class="nf">stepCountIs</span><span class="p">(</span><span class="mi">5</span><span class="p">),</span> <span class="p">});</span> <span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">chunk</span> <span class="k">of</span> <span class="nx">result</span><span class="p">.</span><span class="nx">textStream</span><span class="p">)</span> <span class="p">{</span> <span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="nx">chunk</span><span class="p">);</span> <span class="p">}</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span> </code></pre> </div> <p>That's the whole thing. The agent loop is handled by <code>stepCountIs(5)</code>, the model keeps calling tools until it has enough data to answer, up to 5 steps.</p> <h2> How it works </h2> <p>When you run it, the agent:</p> <ol> <li>Receives your question</li> <li>Decides which tools to call (<code>financeSearch</code>, <code>webSearch</code>, or both)</li> <li>Gets results back. Real prices, actual earnings figures, live news</li> <li>Synthesizes an answer with citations</li> <li>Streams it back token by token</li> </ol> <p>The <code>financeSearch</code> tool pulls from stock prices, earnings reports, income statements, balance sheets, dividend history, and insider transaction data. <code>webSearch</code> covers current news, analyst coverage, and anything else on the open web.</p> <p>For a query like <strong>"What's happening with NVDA?"</strong> you'd typically see it call <code>financeSearch</code> for price and recent earnings, then <code>webSearch</code> for news about data center demand or any recent analyst upgrades, then combine them into a coherent analysis.</p> <h2> Extending it </h2> <p>A few directions from here:</p> <p><strong>Add more tools for deeper research:</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">secSearch</span><span class="p">,</span> <span class="nx">economicsSearch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@valyu/ai-sdk</span><span class="dl">"</span><span class="p">;</span> <span class="nl">tools</span><span class="p">:</span> <span class="p">{</span> <span class="na">financeSearch</span><span class="p">:</span> <span class="nf">financeSearch</span><span class="p">(),</span> <span class="na">webSearch</span><span class="p">:</span> <span class="nf">webSearch</span><span class="p">(),</span> <span class="na">sec</span><span class="p">:</span> <span class="nf">secSearch</span><span class="p">(),</span> <span class="c1">// 10-K, 10-Q, 8-K filings</span> <span class="na">economics</span><span class="p">:</span> <span class="nf">economicsSearch</span><span class="p">(),</span> <span class="c1">// FRED, BLS macro data</span> <span class="p">},</span> </code></pre> </div> <p><strong>Compare multiple tickers:</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="nx">content</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Compare MSFT and GOOGL on trailing P/E, revenue growth, and recent earnings beats</span><span class="dl">"</span> </code></pre> </div> <p>The agent will run parallel searches and give you a side-by-side.</p> <p><strong>Control costs with config options:</strong><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="nf">financeSearch</span><span class="p">({</span> <span class="na">maxNumResults</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="c1">// fewer results = lower cost</span> <span class="na">relevanceThreshold</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">,</span> <span class="c1">// only high-quality matches</span> <span class="p">})</span> </code></pre> </div> <h2> What the data actually covers </h2> <p><code>financeSearch</code> isn't just price tickers. It pulls from:</p> <ul> <li>Historical and real-time stock, crypto, and forex prices</li> <li>Earnings per share, revenue, and guidance</li> <li>Balance sheets, income statements, cash flow</li> <li>Insider buy/sell transactions (Form 4 data)</li> <li>Dividend history</li> </ul> <p>So queries like <strong>"How have TSLA insiders been trading over the last 6 months?"</strong> or <strong>"What's Apple's free cash flow trend since 2021?"</strong> work out of the box.</p> <h2> Running it </h2> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npx tsx agent.ts <span class="s2">"Is AMD a buy right now? Give me current price, latest earnings, and analyst sentiment."</span> </code></pre> </div> <p>Sample output (abridged):<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>AMD is currently trading at $X.XX, down X% today... Recent Earnings (Q4 2024): - Revenue: $X.XB (beat estimates by X%) - EPS: $X.XX vs $X.XX expected - Data center segment grew XX% YoY... Analyst Sentiment: According to recent coverage from [source], consensus is... </code></pre> </div> <p>The numbers are real. The sources are cited. No hallucination.</p> <h2> The Underlying API </h2> <p>Under the hood, <code>@valyu/ai-sdk</code> is a thin wrapper around Valyu's DeepSearch API. If you want to build a custom tool, one that only searches specific financial sources, you can drop down to the raw <code>tool()</code> from the AI SDK:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">tool</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">ai</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">z</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">zod</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">cryptoSearch</span> <span class="o">=</span> <span class="nf">tool</span><span class="p">({</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Search cryptocurrency price and market data</span><span class="dl">"</span><span class="p">,</span> <span class="na">inputSchema</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">object</span><span class="p">({</span> <span class="na">query</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">string</span><span class="p">(),</span> <span class="p">}),</span> <span class="na">execute</span><span class="p">:</span> <span class="k">async </span><span class="p">({</span> <span class="nx">query</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">https://api.valyu.ai/v1/search</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">x-api-key</span><span class="dl">"</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VALYU_API_KEY</span><span class="p">,</span> <span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="nx">query</span><span class="p">,</span> <span class="na">search_type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">proprietary</span><span class="dl">"</span><span class="p">,</span> <span class="na">included_sources</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">valyu/valyu-crypto</span><span class="dl">"</span><span class="p">],</span> <span class="na">max_num_results</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="p">}),</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span> <span class="p">},</span> <span class="p">});</span> </code></pre> </div> <p>Full list of available sources and parameters at <a href="proxy.php?url=https://docs.valyu.ai" rel="noopener noreferrer">docs.valyu.ai</a>.</p> <h2> Code </h2> <p>Full example is a single file, <strong>agent.ts</strong> , above is all you need. If you build something on top of this, the main things to think about are:</p> <ul> <li>System prompt quality matters a lot for financial queries. Be explicit about what each tool is for</li> <li> <strong>stepCountIs(5)</strong> is usually enough for single-stock analysis; bump to 8-10 for comparative research</li> <li>Stream the output for better UX - <strong>streamText</strong> over <strong>generateText</strong> </li> </ul> <p>The Valyu finance data covers benchmarks like 73% accuracy on 120 finance questions vs 55% for Google search, which matters when you're asking specific questions about earnings or insider activity where precision is the point.</p> ai agents typescript valyu A Code-First Approach to Managing Notification Workflows Prosper Otemuyiwa Thu, 09 May 2024 13:50:37 +0000 https://dev.to/novu/a-code-first-approach-to-managing-notification-workflows-j8j https://dev.to/novu/a-code-first-approach-to-managing-notification-workflows-j8j <p>Building rich, smooth, and customizable multi-channel notification experiences that are delivered the right way is difficult. Scaling them across teams and organizations makes it even harder.</p> <p>At Novu, we empower developers with best-in-class notification tools and infrastructure to make managing notifications a walk-in-the-park when developing apps or any form of digital system.</p> <h2> Why a code-first notification workflow approach? </h2> <p>These are the current challenges developers face in managing notification workflows:</p> <ul> <li> <strong>ClickOps, not GitOps</strong>. Currently, developers are limited to building notification workflows solely through the user interface (UI) and API. While these methods serve their purpose adequately, they come with some drawbacks. Not only are they time-consuming, but they also disrupt the development flow.</li> <li> <strong>External Frameworks.</strong> It’s inherently difficult and sometimes impossible to integrate notification workflows with external content frameworks. There are tons of tools out there that simplify notification content creation.</li> <li> <strong>Debugging.</strong> Debugging notification content issues in a UI or via API can be time-consuming.</li> <li> <strong>Limited Customization.</strong> Immense limitation of notification steps customization and lack of control. Developers yearn for the ability to control the before and after of certain actions and events in their apps. The if this, then that approach is not readily available in a UI-first notification workflow.</li> <li> <strong>Huge barrier for non-technical users.</strong> Developers rarely work in silos. They work together with marketing, product, sales and other departments to build, manage and sell software. Currently, non-technical users depend on them for the slightest modifications and changes to the notification experience. It’s time-consuming and not scalable!</li> </ul> <h2> The power of code-first notification workflows </h2> <p>The code-first notification workflow approach empowers you as a developer to build advanced workflows with code while giving your non-technical teammates full control over content and behaviour.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F450devm8midto9bvzy1v.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F450devm8midto9bvzy1v.png" alt="code-first approach"></a></p> <p>Everyone wants to be great at something - a great friend, a great partner, a great footballer, a great chef, a great colleague. Novu’s advanced code-first workflow (<strong>currently known as Novu Echo</strong>) makes you a great developer and teammate by arming you with incredible power to do the following:</p> <ul> <li>Integration of content frameworks and templating libraries such as <strong>React Email</strong>, <strong>MJML</strong>, <strong>Vuemail</strong>, and <strong>Maizzle</strong> into your workflow. As long as it’s an npm package or library, you can integrate it with your notification workflow.</li> <li>Run custom code between notification steps (email, SMS, in-app, push).- For example, before sending an email, get data from an external service or DB. <ul> <li>Pass custom data from any component to channel providers within the code.</li> <li>Pass customized and well-designed content data into the <strong>In-App</strong> notification step.</li> </ul> </li> </ul> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">import</span> <span class="p">{</span> <span class="nx">Echo</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/echo</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">renderReactEmail</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./emails/react-email</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">commentWorkflow</span> <span class="o">=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">workflow</span><span class="p">(</span><span class="dl">'</span><span class="s1">comment-on-post</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="kd">function</span><span class="p">({</span> <span class="nx">step</span><span class="p">,</span> <span class="nx">subscriber</span><span class="p">,</span> <span class="nx">payload</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">inAppResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">step</span><span class="p">.</span><span class="nf">inApp</span><span class="p">(</span><span class="dl">'</span><span class="s1">in-app-step</span><span class="dl">'</span><span class="p">,</span> <span class="k">async </span><span class="p">(</span><span class="nx">inputs</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">body</span><span class="p">:</span> <span class="nf">renderReactComponent</span><span class="p">(</span><span class="nx">inputs</span><span class="p">)</span> <span class="p">}</span> <span class="p">},</span> <span class="p">{</span> <span class="na">inputs</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// ...JSON Schema or ZOD/Ajv/Class Validators definition</span> <span class="p">}</span> <span class="p">});</span> <span class="c1">// Novu Worker Engine will manage the state and durability of each step in isolation</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">step</span><span class="p">.</span><span class="nf">digest</span><span class="p">(</span><span class="dl">'</span><span class="s1">diges-events</span><span class="dl">'</span><span class="p">,</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">amount</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">unit</span><span class="p">:</span> <span class="dl">'</span><span class="s1">day</span><span class="dl">'</span> <span class="p">};</span> <span class="p">});</span> <span class="k">await</span> <span class="nx">step</span><span class="p">.</span><span class="nf">email</span><span class="p">(</span><span class="dl">'</span><span class="s1">comment-reminder</span><span class="dl">'</span><span class="p">,</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Echo runs as part of your application, so you have access to your database or resources</span> <span class="kd">const</span> <span class="nx">post</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nf">findPost</span><span class="p">(</span><span class="nx">payload</span><span class="p">.</span><span class="nx">post_id</span><span class="p">);</span> <span class="k">return</span> <span class="p">{</span> <span class="na">subject</span><span class="p">:</span> <span class="dl">'</span><span class="s1">E-mail Subject</span><span class="dl">'</span><span class="p">,</span> <span class="na">body</span><span class="p">:</span> <span class="nf">renderReactEmail</span><span class="p">(</span><span class="nx">inputs</span><span class="p">,</span> <span class="nx">events</span><span class="p">)</span> <span class="p">}</span> <span class="p">},</span> <span class="p">{</span> <span class="c1">// Step-level inputs defined in code and controlled in the novu Cloud UI by a Non Technical Team member</span> <span class="na">inputSchema</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// ...JSON Schema</span> <span class="p">},</span> <span class="na">providers</span><span class="p">:</span> <span class="p">{</span> <span class="na">sendgrid</span><span class="p">:</span> <span class="k">async </span><span class="p">(</span><span class="nx">inputs</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">ipPoolName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">custom-pool</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span> <span class="na">skip</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Write custom skip logic</span> <span class="k">return</span> <span class="nx">inAppResponse</span><span class="p">.</span><span class="nx">seen</span><span class="p">;</span> <span class="p">}</span> <span class="p">});</span> <span class="p">},</span> <span class="p">{</span> <span class="c1">// Define your workflow trigger payload using json schema and custom validation;</span> <span class="na">payloadSchema</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// ...JSON Schema</span> <span class="p">}</span> <span class="p">});</span> <span class="c1">// Trigger workflows like you usually do</span> <span class="nx">commentWorkflow</span><span class="p">.</span><span class="nf">trigger</span><span class="p">({</span> <span class="na">to</span><span class="p">:</span> <span class="p">{</span> <span class="na">subscriberId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">12345</span><span class="dl">'</span> <span class="p">},</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="p">...</span><span class="nx">Custom</span> <span class="nx">Trigger</span> <span class="nx">Data</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <p><em>An example of the code-first notification workflow</em></p> <ul> <li>Build reusable workflow components. For instance, your codebase can now have rich and complete email, In-App and push notification components all in the same place.</li> <li>Integrate with existing legacy design and notification systems.</li> <li>Version-controlled notification workflows managed in Git. <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzu058jeuja2n2zpzz91j.png" alt="notification workflows managed in Git"> </li> <li>Debug and replay notification workflows in your code editors.</li> <li>Define workflow inputs and variables using JSON schema (supports zod, ajv, class validators, and so many more)</li> </ul> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="p">...</span> <span class="kd">const</span> <span class="nx">inAppResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">step</span><span class="p">.</span><span class="nf">inApp</span><span class="p">(</span><span class="dl">'</span><span class="s1">in-app-step</span><span class="dl">'</span><span class="p">,</span> <span class="k">async </span><span class="p">(</span><span class="nx">inputs</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">body</span><span class="p">:</span> <span class="nf">renderReactComponent</span><span class="p">(</span><span class="nx">inputs</span><span class="p">)</span> <span class="p">}</span> <span class="p">},</span> <span class="p">{</span> <span class="na">inputSchema</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">,</span> <span class="na">properties</span><span class="p">:</span> <span class="p">{</span> <span class="na">prompt</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">System Message</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span> <span class="na">default</span><span class="p">:</span> <span class="dl">"</span><span class="s2">This is the default message</span><span class="dl">"</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The system message to be sent.</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span> <span class="na">showCount</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Show Digest Count</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span><span class="p">,</span> <span class="na">default</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Whether to show the count of the messages.</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span> <span class="na">showSummary</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Show Digest Summary</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">boolean</span><span class="dl">"</span><span class="p">,</span> <span class="na">default</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Whether to show the summary of the messages.</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span> <span class="p">},</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <p><em>input using JSON schema</em></p> <ul> <li>Deploy changes from your local development environment to the Novu Cloud in your CI platform.</li> </ul> <h2> GitOps notification as code </h2> <p>Below is a glimpse of an engineer crafting the notification workflow and content locally using the Novu Dev Studio. Any changes made locally are synced to the Cloud via the click of a button by the engineer.</p> <ul> <li>The UI components (step inputs) on the right are built from code by the engineer. <em>The result of the code shown earlier.</em> </li> <li>The email content is designed using React email.</li> <li>The JSON tab displays editable JSON code for making changes <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfilrqskap5jqp5t248j.png" alt="The Novu Dev Studio"><em>The Novu Dev Studio</em> </li> </ul> <h2> No-code control to non-technical users </h2> <p>One major pain point that Novu Echo addresses is the inability for non-technical users to modify parts of the notification workflow as they see fit.</p> <p>Now, engineers can develop complex notification workflows from code, which exposes a customized UI interface (<em>as shown above</em>) with step inputs that product, marketing, and any non-technical user can safely use to modify the notification experience without breaking the system.</p> <h2> Power has changed hands—from constraint to limitless potential </h2> <p>The code-first notification workflow approach enhances the process of sending and managing notifications. It has put boundless and great power in the hands of engineers, enabling them to bring to life any type of notification systems and engines they imagine.</p> <p>With this approach, engineers can build notification systems that offer unparalleled flexibility and customization, benefiting their customers, teammates, and companies alike.</p> <ul> <li>No longer limited by UI notification steps and rigidity.</li> <li>No longer limited by notification content editors and systems. The more, the merrier!</li> <li>Now an IFTTT (If-This-Then-That) pro engineer!</li> </ul> <p>Delve into building right away with this approach by using our <a href="proxy.php?url=https://docs.novu.co/echo/quickstart" rel="noopener noreferrer">code-first notification workflow guide.</a>.</p> webdev notifications novu echo How to build dev.to Community Digest with Novu Prosper Otemuyiwa Mon, 29 Apr 2024 18:20:30 +0000 https://dev.to/novu/how-to-build-devto-community-digest-with-novu-376m https://dev.to/novu/how-to-build-devto-community-digest-with-novu-376m <p><a href="proxy.php?url=http://dev.to/">dev.to</a> is a widely recognized and highly esteemed community dedicated to developers from all around the globe. It serves as a platform for thousands of developers to learn, share, and publish their experiences with leveraging technology in their work.</p> <p>One other thing that piqued my curiosity is the <a href="proxy.php?url=http://dev.to">dev.to</a> community Digest I frequently get in my inbox. The email digest helps me stay informed about some of the top articles I am interested in.</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--UFeoPFOb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-22-at-15.14.13%402x-1-1024x648.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--UFeoPFOb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-22-at-15.14.13%402x-1-1024x648.png" alt="" width="800" height="506"></a><em><a href="proxy.php?url=http://Dev.to">Dev.to</a> Email Community Digest</em></p> <p>In the <a href="proxy.php?url=https://novu.co/blog/how-to-build-dev-to-in-app-notification-system-in-20-minutes/">first part of this article</a>, I covered how to develop a system similar to <a href="proxy.php?url=http://dev.to/">dev.to</a>'s In-App Notification system in less than 30 minutes. Now, I'll show you how to build an automated email community digest similar to this using Novu.</p> <h2> Add a Digest &amp; Email Node to Existing Workflow on Novu </h2> <p>All notifications are sent via a workflow. A workflow acts as a container for the logic and blueprint associated with any type of notification in your app.</p> <p>All the channels (Email, SMS, In-App, Push) are tied together under a single entity in a workflow.</p> <p>In the previous article, we already created a workflow. Now, let's add a Digest node to it:</p> <ul> <li>Open the existing <code>Devto Notifications</code> <strong>Workflow</strong>.</li> <li>Click on the + icon under the <strong>In-App</strong> node and select <strong>Digest</strong>.</li> </ul> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--o_M2avsT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-11.30.49%402x-1024x587.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--o_M2avsT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-11.30.49%402x-1024x587.png" alt="" width="800" height="459"></a>- Click on the <strong>Digest</strong> node. A modal will open where you can set how long events need to be digested before the next action. In this case, the next action will be sending an email to subscribers.</p> <ul> <li> Set 1 minute for the digest. Feel free to experiment with this. You can set any custom minute or hour needed.</li> <li> The <strong><code>digest</code></strong> node allows you to set a specific time interval for when notifications should be sent.</li> <li> We need to access all the digested event payloads in order to show the subscriber all or parts of the events included in this digest. For example: <strong>“Prosper and 2 others liked your photo.”</strong> </li> <li> In our case, we need all post titles and links that have been published by anyone in the past minute to be sent in one email to subscribers.</li> </ul> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--h0Pbo4i---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-11.33.47%402x-1024x580.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--h0Pbo4i---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-11.33.47%402x-1024x580.png" alt="" width="800" height="453"></a>- Click on the + icon again to add the <strong>Email</strong> channel.</p> <ul> <li> <p>Click on the <strong>Email</strong> channel node to add content. Don't forget to click the <strong>Update</strong> button when you're finished.</p> <ul> <li>Add the content below to it. This is a digest template: </li> </ul> </li> </ul> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span> <span class="nx">Hey</span> <span class="nx">Dev</span><span class="p">.</span><span class="nx">to</span> <span class="nx">Subscriber</span><span class="p">,</span> <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="nx">Here</span> <span class="nx">are</span> <span class="nx">some</span> <span class="nx">recent</span> <span class="nx">community</span> <span class="nx">posts</span> <span class="nx">just</span> <span class="k">for</span> <span class="nx">you</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="p">{{</span><span class="err">#</span><span class="nx">each</span> <span class="nx">step</span><span class="p">.</span><span class="nx">events</span><span class="p">}}</span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">{{article_url}}</span><span class="dl">"</span> <span class="nx">style</span><span class="o">=</span><span class="dl">"</span><span class="s2">font-weight:bold;</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h3</span><span class="o">&gt;</span><span class="p">{{</span><span class="nx">article_title</span><span class="p">}}</span><span class="o">&lt;</span><span class="sr">/h3</span><span class="err">&gt; </span><span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span><span class="o">&lt;</span><span class="nx">br</span><span class="o">/&gt;</span> <span class="p">{{</span><span class="o">/</span><span class="nx">each</span><span class="p">}}</span> </code></pre> </div> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--tVR9v9VB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-10.54.29%402x-1-1024x590.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--tVR9v9VB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-10.54.29%402x-1-1024x590.png" alt="" width="800" height="461"></a>As part of the digested template, you will have access to a few properties:</p> <ul> <li> <strong>step.events</strong>: An array of all the events aggregated under this digest.</li> <li> <strong>step.total_count</strong>: The number of total events in this digest.</li> <li> <strong>step.digest</strong>: A boolean flag to indicate if we are in digest mode right now.</li> </ul> <p>In the code above, we are looping through the <strong>step.events</strong> to grab all the events that have happened in the action before sending an email.</p> <p>The action here is on In-App push notifications. Novu collects all the In-App push notifications that occur within a one-minute digest time. It then uses the specified payload (in this case, <strong>article_url</strong> and <strong>article_title</strong>) to determine the content to display in the email sent to subscribers. It's quite impressive!</p> <h2> Publish to dev.to via the API with POSTMAN or Insomnia </h2> <p>Fire up either <strong>POSTMAN</strong> or <strong>Insomnia</strong> again and publish as many articles as possible within the space of a minute.</p> <p>Firstly, the In-App notifications will appear as intended. No surprises here. We already went through this in the <a href="proxy.php?url=https://novu.co/blog/how-to-build-dev-to-in-app-notification-system-in-20-minutes/">previous part of this series</a>.</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--BYnnDiSG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-11.55.41%402x-1024x637.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--BYnnDiSG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-11.55.41%402x-1024x637.png" alt="" width="800" height="498"></a>Wait for a minute. You should receive an email after a minute with all of the articles published within that timeframe.</p> <p>The email just arrived in my inbox. Just like on <a href="proxy.php?url=http://dev.to">dev.to</a>!</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--CcfEkYxg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-10.58.01%402x-1024x166.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--CcfEkYxg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-10.58.01%402x-1024x166.png" alt="" width="800" height="130"></a>The body of the email contains a digest of all the content published within a minute.</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--943UEn92--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-10.58.44%402x-1024x509.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--943UEn92--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-29-at-10.58.44%402x-1024x509.png" alt="" width="800" height="398"></a>That's all. Now, we have a <a href="proxy.php?url=http://dev.to">dev.to</a> email community digest.</p> <p><strong>Note:</strong> Feel free to style the email content to look as pretty as dev.to’s community digest. </p> <h2> More on Using Digest </h2> <p>There are numerous scenarios where using a notification digest in your app can be beneficial.</p> <ul> <li>For instance, if you're developing a social media app like Instagram, TikTok, or a LinkedIn-like app that requires frequent user interaction notifications, it's wise to digest these notifications. To avoid overwhelming users and risking app uninstalls or notification disabling, manage your notification volume effectively.</li> <li>Similarly, if you're building the next GitHub or a web app requiring constant email notifications, take care with your notification strategy. Using an email digest can be a practical solution!</li> </ul> <p>For more information, refer to this <a href="proxy.php?url=https://docs.novu.co/workflows/digest">comprehensive guide on the different types of digests available</a> and learn how you can configure them in your app with Novu.</p> <h2> Conclusion </h2> <p>We have successfully incorporated an email community digest into our app without making any changes to the <a href="proxy.php?url=https://github.com/novuhq/novu-devto">existing codebase</a>. We only made a few changes in our Novu dashboard.</p> <p>I prayed for days like these a decade ago. They're here and they powerfully demonstrate how we can leverage excellent tools to move quickly, iterate, focus on business logic and build a million-dollar business <strong>😁</strong></p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--qD294WmY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/IMG_7944-1.jpg" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--qD294WmY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://manage.novu.co/wp-content/uploads/2024/04/IMG_7944-1.jpg" alt="" width="800" height="765"></a>I’ll leave you with these two guides that show in detail how to:</p> <ul> <li><a href="proxy.php?url=https://docs.novu.co/guides/add-digest-to-email-notifications">Add digest to Email notifications</a></li> <li><a href="proxy.php?url=https://docs.novu.co/guides/add-digest-to-inapp-notifications">Add digest to In-App notifications</a></li> </ul> <p>Don’t hesitate to ask any questions or request support. You can find me on <a href="proxy.php?url=https://bit.ly/3QfOQHp">Discord</a> and <a href="proxy.php?url=https://bit.ly/3JCpHCR">Twitter</a>. Please feel free to reach out.</p> javascript devto notifications digest How to build dev.to In-App Notification System in 20 minutes Prosper Otemuyiwa Tue, 23 Apr 2024 11:14:26 +0000 https://dev.to/novu/how-to-build-devto-in-app-notification-system-in-20-minutes-2m98 https://dev.to/novu/how-to-build-devto-in-app-notification-system-in-20-minutes-2m98 <p><a href="proxy.php?url=https://bit.ly/3WaNvoU" rel="noopener noreferrer">dev.to</a> is a very popular community for developers. It’s a platform for thousands of developers to collaborate, learn, publish and explore new ways of making programming languages and technology work for them.</p> <p>I’m an avid reader of <a href="proxy.php?url=https://bit.ly/3WaNvoU" rel="noopener noreferrer">dev.to</a> and publish a lot of articles on the platform. My favorite thing to do is to check my <a href="proxy.php?url=https://bit.ly/3Jxs1LA" rel="noopener noreferrer">notifications</a> to see who posted a new article, commented or liked any of my posts.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-16.15.21%402x-1024x687.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-16.15.21%402x-1024x687.png" alt="Guide"></a>I'll guide you on how to swiftly build an In-App Notification system for your next app using <a href="proxy.php?url=https://novu.co" rel="noopener noreferrer">Novu</a> and the <a href="proxy.php?url=https://bit.ly/3Jxs1LA" rel="noopener noreferrer">dev.to</a> API. While it might not exactly resemble the image mentioned above, it'll have many similarities.</p> <p>If you want to explore the code right away, you can <strong><a href="proxy.php?url=https://bit.ly/3Jwq69X" rel="noopener noreferrer">view the completed code</a></strong> <a href="proxy.php?url=https://github.com/novuhq/react-translation-app" rel="noopener noreferrer"></a> on GitHub.</p> <h2> Grab your dev.to API Key </h2> <p>You can get your dev.to API key from your <a href="proxy.php?url=https://bit.ly/3UexkEy" rel="noopener noreferrer">settings page</a>.</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-16.25.47%402x-1024x539.png" alt="Set Up">Set Up a Next.js App </h2> <p>To create a Next.js app, open your terminal, <code>cd</code> into the directory you’d like to create the app in, and run the following command:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npx create-next-app@latest devto </code></pre> </div> <p>Go through the prompts, select your preference, install and run the app in your browser with:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2Fpreference-1024x658.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2Fpreference-1024x658.png" alt="Prompts"></a></p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npm run dev </code></pre> </div> <h2> Set Up &amp; Integrate Novu </h2> <p>Run the following command to install the Novu node SDK:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npm <span class="nb">install</span> @novu/node </code></pre> </div> <p>Run the following command to install the Novu Notification Center package:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npm <span class="nb">install</span> @novu/notification-center </code></pre> </div> <p>The Novu Notification Center package provides a React component library that adds a notification center to your React app. The <a href="proxy.php?url=https://docs.novu.co/notification-center/introduction#ui-libraries" rel="noopener noreferrer">package is also available for non-React apps</a>.</p> <p>Before we can start sending and receiving notifications, we need to set up a few things:</p> <ol> <li>Create a workflow for sending notifications,</li> <li>Create a subscriber - recipient of notifications.</li> </ol> <h2> <strong><a href="proxy.php?url=https://docs.novu.co/quickstarts/react#create-a-workflow" rel="noopener noreferrer"></a>Create a Novu Workflow</strong> </h2> <p>A workflow is a blueprint for notifications. It includes the following:</p> <ul> <li>Workflow name and Identifier</li> <li>Channels: - Email, SMS, Chat, In-App and Push</li> </ul> <p>Create a workflow by following the steps below:</p> <ol> <li>Click <strong>Workflow</strong> on the left sidebar of your Novu dashboard.</li> <li>Click the <strong>Add a Workflow</strong> button on the top left. You can select a Blank workflow or use one of the existing templates.</li> <li>The name of the new workflow is currently <strong>“Untitled”</strong>. Rename it to <code>Devto Notifications</code>.</li> <li>Select <strong>In-App</strong> as the channel you want to add.</li> </ol> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2Fimage-8-1024x548.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2Fimage-8-1024x548.png" alt="In-App Channel"></a>5. Click on the recently added <strong>“In-App”</strong> channel and add the following text to it. Once you’re done, click <strong>“Update”</strong> to save your configuration.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.19.47%402x-1024x588.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.19.47%402x-1024x588.png" alt="Add text"></a></p> <p>The code below is what is in the image above. You can copy &amp; paste it into the Novu editor.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;&lt;</span><span class="nx">strong</span><span class="o">&gt;</span><span class="p">{{</span> <span class="nx">name</span> <span class="p">}}</span><span class="o">&lt;</span><span class="sr">/strong&gt; made a new post.&lt;/</span><span class="nx">p</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">br</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nx">article</span> <span class="nx">style</span><span class="o">=</span><span class="dl">"</span><span class="s2">padding:10px;border-radius:5px;border-color: white;border-block: solid;</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">{{ article_url }}</span><span class="dl">"</span> <span class="nx">style</span><span class="o">=</span><span class="dl">"</span><span class="s2">font-weight:bold;</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h3</span><span class="o">&gt;</span><span class="p">{{</span> <span class="nx">article_title</span> <span class="p">}}</span><span class="o">&lt;</span><span class="sr">/h3</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span><span class="o">&lt;</span><span class="sr">/article</span><span class="err">&gt; </span></code></pre> </div> <p>Enable the “Add an Avatar” button to allow the notifications coming in show a default avatar.</p> <h2> <strong><a href="proxy.php?url=https://docs.novu.co/quickstarts/react#create-a-subscriber" rel="noopener noreferrer"></a>Create a Subscriber to receive Notifications</strong> </h2> <p>A subscriber is the recipient of a notification. Essentially, your app users are subscribers. As a logged-in user on <a href="proxy.php?url=http://dev.to">dev.to</a>, you are a subscriber.</p> <p>To send notifications to a user on your app, you’ll need to register that user as a subscriber on Novu. If you click <strong>“Subscriber”</strong> on the left sidebar of the <strong><a href="proxy.php?url=https://web.novu.co/subscribers" rel="noopener noreferrer">Novu dashboard</a></strong>, you’ll see the subscriber list. As a first time Novu user, it will be an empty list.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-03-14-at-07.19.45%402x-2-1024x564.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-03-14-at-07.19.45%402x-2-1024x564.png" alt="Create a Subscriber"></a>Open your terminal and run the following script to create a subscriber:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>curl <span class="nt">--location</span> <span class="s1">'&lt;https://api.novu.co/v1/subscribers&gt;'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Accept: application/json'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Authorization: ApiKey &lt;NOVU_API_KEY&gt;'</span> <span class="se">\</span> <span class="nt">--data-raw</span> <span class="s1">'{ "firstName": "John", "lastName": "Doe", "email": "[email protected]", "phone": "+1234567890" }'</span> </code></pre> </div> <p><strong>Note:</strong> Grab your <strong>NOVU API Key</strong> from the <a href="proxy.php?url=https://web.novu.co/settings" rel="noopener noreferrer">settings section</a> of your Novu dashboard.</p> <p>Refresh the Subscribers page on your Novu dashboard. You should see the recently added subscriber now! You can also add a subscriber to Novu by running this <a href="proxy.php?url=https://docs.novu.co/api-reference/subscribers/create-subscriber" rel="noopener noreferrer">API endpoint</a>.</p> <p><strong>Note:</strong> The best option to add a subscriber is via code in your backend. With Node.js code, you can run the following code to create a subscriber:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Novu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/node</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// Insert your Novu API Key here</span> <span class="kd">const</span> <span class="nx">novu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Novu</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;NOVU_API_KEY&gt;</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// Create a subscriber on Novu</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nx">subscribers</span><span class="p">.</span><span class="nf">identify</span><span class="p">(</span><span class="dl">"</span><span class="s2">132</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">email</span><span class="p">:</span> <span class="dl">"</span><span class="s2">[email protected]</span><span class="dl">"</span><span class="p">,</span> <span class="na">firstName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">John</span><span class="dl">"</span><span class="p">,</span> <span class="na">lastName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Doe</span><span class="dl">"</span><span class="p">,</span> <span class="na">phone</span><span class="p">:</span> <span class="dl">"</span><span class="s2">+13603963366</span><span class="dl">"</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <p>For the sake of this article, we’ll make do with adding a subscriber via the terminal to speed the process up.</p> <h2> <strong>Set up Novu Notification Center in the App</strong> </h2> <p>Head over to <code>scr/pages/index.js</code>. We’ll modify this page to include the Novu Notification Center.</p> <p>Import the Notification Center components from the Novu notification center package like so:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="p">...</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">NovuProvider</span><span class="p">,</span> <span class="nx">PopoverNotificationCenter</span><span class="p">,</span> <span class="nx">NotificationBell</span><span class="p">,</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/notification-center</span><span class="dl">"</span><span class="p">;</span> </code></pre> </div> <p>Display the Notification Center by adding the imported components like so:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="p">...</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">main</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`flex min-h-screen flex-col items-center justify-between p-24 </span><span class="p">${</span><span class="nx">inter</span><span class="p">.</span><span class="nx">className</span><span class="p">}</span><span class="s2">`</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30</span><span class="dl">"</span><span class="o">&gt;</span> <span class="nx">Dev</span><span class="p">.</span><span class="nx">to</span> <span class="nx">In</span><span class="o">-</span><span class="nx">App</span> <span class="nx">Notifications</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">NovuProvider</span> <span class="nx">subscriberId</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">}</span> <span class="nx">applicationIdentifier</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_APP_ID</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">PopoverNotificationCenter</span><span class="o">&gt;</span> <span class="p">{({</span> <span class="nx">unseenCount</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">NotificationBell</span> <span class="nx">unseenCount</span><span class="o">=</span><span class="p">{</span><span class="nx">unseenCount</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span> <span class="p">)}</span> <span class="o">&lt;</span><span class="sr">/PopoverNotificationCenter</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/NovuProvider</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">...</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">grid grid-cols-2</span><span class="dl">"</span><span class="o">&gt;</span> </code></pre> </div> <p>Check your app, you should immediately see a notification bell.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-17.00.36%402x-1024x749.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-17.00.36%402x-1024x749.png" alt="Notification bell"></a>Click the notification bell. The Notification Center will immediately pop up like so:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-16.59.44%402x-1024x665.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-16.59.44%402x-1024x665.png" alt="Env variables"></a>Add the following to your <code>.env</code> values:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="o">=</span> <span class="nx">NEXT_PUBLIC_NOVU_APP_ID</span><span class="o">=</span> <span class="nx">NEXT_PUBLIC_NOVU_API_KEY</span><span class="o">=</span> </code></pre> </div> <p>Grab your <strong>Novu API Key</strong> and <strong>APP ID</strong> from the <a href="proxy.php?url=https://web.novu.co/settings" rel="noopener noreferrer">Settings</a> section of your Novu dashboard.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-03-14-at-07.52.14%402x-1-1024x542.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-03-14-at-07.52.14%402x-1-1024x542.png" alt="Get the Subscriber ID"></a>Get the Identifier of the subscriber we created earlier and add it to the <code>.env</code> values.</p> <p>In a real world app, the ID of the logged-in user should be passed to the <strong>subscriberID</strong> property of the <strong>NovuProvider</strong> component.</p> <p><strong>Note</strong>: We’re using it as an env variable because we’re not incorporating an authentication system in this tutorial. Ideally, once the user registers, the user ID should immediately be created as a subscriber and the ID be passed to the component.</p> <p>Reload your app and the notification center should be squeaky clean like so:</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-17.16.14%402x-1024x641.png" alt="Build the API">Build the API to Publish <a href="proxy.php?url=http://dev.to">dev.to</a> Articles &amp; Trigger In-App Notifications </h2> <p>Create a <code>publish-devto-article.js</code> file in the <code>src/pages/api</code> directory.</p> <p>Add the code below to it to import the Novu SDK and specify the workflow trigger ID:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Novu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/node</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">novu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Novu</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_API_KEY</span><span class="p">);</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">workflowTriggerID</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">devto-notifications</span><span class="dl">"</span><span class="p">;</span> </code></pre> </div> <p><strong>Note:</strong> The <code>@novu/node</code> Novu SDK can be used only in server-side.</p> <p>The <code>workflowTriggerID</code> value is obtained from the workflow dashboard. Earlier, when we set up a workflow titled <code>Devto Notifications</code>, Novu created a slug from this title to serve as the trigger ID.</p> <p>You can see it here:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.11.33%402x-1024x593.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.11.33%402x-1024x593.png" alt="Add the code below"></a>Now add the following functions to the code in the <code>publish-devto-article.js</code> file:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="p">...</span> <span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">handler</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">article</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;https://dev.to/api/articles&gt;</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">API-Key</span><span class="dl">"</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_DEVTO_API_KEY</span><span class="p">,</span> <span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="na">article</span><span class="p">:</span> <span class="nx">article</span> <span class="p">}),</span> <span class="p">});</span> <span class="cm">/** * Get response of the published article from Dev.to */</span> <span class="kd">const</span> <span class="nx">dataFromDevto</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span> <span class="cm">/** * Extra the essential details needed from the Dev.to response */</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">title</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">published_timestamp</span><span class="p">,</span> <span class="nx">user</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">dataFromDevto</span><span class="p">;</span> <span class="cm">/** * Send notification that a new article has been published */</span> <span class="nf">sendInAppNotification</span><span class="p">(</span><span class="nx">user</span><span class="p">,</span> <span class="nx">title</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">({</span> <span class="na">message</span><span class="p">:</span> <span class="nx">dataFromDevto</span> <span class="p">});</span> <span class="p">}</span> <span class="cm">/** * SEND IN-APP NOTIFICATION VIA NOVU * @param {*} userDetails * @param {*} articleTitle * @param {*} articleURL * @returns json */</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">sendInAppNotification</span><span class="p">(</span><span class="nx">userDetails</span><span class="p">,</span> <span class="nx">articleTitle</span><span class="p">,</span> <span class="nx">articleURL</span><span class="p">)</span> <span class="p">{</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nf">trigger</span><span class="p">(</span><span class="nx">workflowTriggerID</span><span class="p">,</span> <span class="p">{</span> <span class="na">to</span><span class="p">:</span> <span class="p">{</span> <span class="na">subscriberId</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">,</span> <span class="p">},</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">userDetails</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">article_url</span><span class="p">:</span> <span class="nx">articleURL</span><span class="p">,</span> <span class="na">article_title</span><span class="p">:</span> <span class="nx">articleTitle</span><span class="p">,</span> <span class="na">profile_image</span><span class="p">:</span> <span class="nx">userDetails</span><span class="p">.</span><span class="nx">profile_image_90</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">({</span> <span class="na">finish</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>The <code>sendInAppNotification</code> function block of code above triggers the notification via the Novu SDK:</p> <ul> <li>Accepts the workflow trigger ID to determine which workflow to trigger.</li> <li>Accepts the subscriber ID value to identify the notification recipient.</li> <li>Accepts a payload object that represents the parameters to inject into the workflow variables.</li> </ul> <p>The <code>handler</code> function block above makes a POST request to <a href="proxy.php?url=http://Dev.to" rel="noopener noreferrer">Dev.to</a> to publish an article and then calls the <code>sendInAppNotification</code> to fire an In-App notification indicating that someone just published an article.</p> <p><strong>Note:</strong> The request body is passed from POSTMAN or Insomnia. Next, we will test our API with any of these API software tools.</p> <h2> Test the API with POSTMAN or Insomnia - Publish to <a href="proxy.php?url=http://Dev.to" rel="noopener noreferrer">Dev.to</a> </h2> <p>Fire up either <strong>POSTMAN</strong> or <strong>Insomnia</strong> and make a request to the API we created and post a JSON request to publish an article on <a href="proxy.php?url=http://Dev.to" rel="noopener noreferrer">Dev.to</a>.</p> <p><strong>Our API url:</strong> <a href="proxy.php?url=http://localhost:3000/api/publish-devto-article*" rel="noopener noreferrer">http://localhost:3000/api/publish-devto-article</a></p> <p>I personally use &amp; prefer Insomnia because it has a snappy and lovely UI. As you can see below, we passed some parameters (pulled from <a href="proxy.php?url=https://bit.ly/3Qf8CTA" rel="noopener noreferrer">dev.to</a> <a href="proxy.php?url=https://developers.forem.com/api/v1#tag/articles/operation/createArticle" rel="noopener noreferrer">API docs</a>) to enable us publish an article.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.27.12%402x-1024x745.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.27.12%402x-1024x745.png" alt="Instant Notification"></a>Next, check your app. You should see an instant notification about the new published article on <a href="proxy.php?url=http://Dev.to" rel="noopener noreferrer">Dev.to</a>.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.29.15%402x-1024x690.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.29.15%402x-1024x690.png" alt="link to article"></a>Click on the new post title. It should take you directly to the article on <a href="proxy.php?url=http://Dev.to" rel="noopener noreferrer">Dev.to</a>!</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.30.55%402x-1024x671.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.30.55%402x-1024x671.png" alt="Publish away"></a>You can try publishing as many as possible.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.33.21%402x-1024x647.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-19.33.21%402x-1024x647.png" alt="Notification Center"></a>The Notification Center provides a lot of functions built-in. A user can mark all notifications as read, mark one notification as read, and delete a notification.</p> <p>The Notification Center ships with a lot of <a href="proxy.php?url=https://docs.novu.co/notification-center/client/react/customization" rel="noopener noreferrer">props and options to customize the whole experience</a>. <a href="proxy.php?url=https://docs.novu.co/notification-center/client/react/api-reference" rel="noopener noreferrer">Header, footer, colors</a> can be customized to conform with your app.</p> <h2> Center the Notification Center, Just like <a href="proxy.php?url=http://Dev.to" rel="noopener noreferrer">Dev.to</a>’s </h2> <p>You might be wondering, <a href="proxy.php?url=http://dev.to/">Dev.to</a>’s Notification Center isn't a popover; it's centered on the page. How do I achieve that?</p> <p>Simply replace the <code>PopoverNotificationCenter</code> component with the <code>NotificationCenter</code> component from the Novu package.</p> <p>It should look something like this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="o">&lt;</span><span class="nx">NovuProvider</span> <span class="nx">subscriberId</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">}</span> <span class="nx">applicationIdentifier</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_APP_ID</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">NotificationCenter</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/NovuProvider</span><span class="err">&gt; </span></code></pre> </div> <p>However, Next.js uses Server side rendering so you might come across this issue on your page because the <code>&lt;NotificationCenter /&gt;</code> component makes use of the window object.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-23-at-10.12.20%402x-1024x479.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-23-at-10.12.20%402x-1024x479.png" alt="SSR issues"></a>Let’s fix this issue by wrapping the component in a way that disables ssr and lazy loads it.</p> <p>Create a <code>components/notifications</code> folder in the <code>src/</code> directory. Go ahead and add these two files with the code below to them respectively:</p> <p><code>index.js</code><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="k">export</span> <span class="p">{</span> <span class="k">default</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./notifications</span><span class="dl">"</span><span class="p">;</span> </code></pre> </div> <p><code>notifications.js</code><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">NotificationCenter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/notification-center</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">dynamic</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/dynamic</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useCallback</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">Notifications</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">onNotificationClick</span> <span class="o">=</span> <span class="nf">useCallback</span><span class="p">((</span><span class="nx">notification</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if </span><span class="p">(</span><span class="nx">notification</span><span class="p">?.</span><span class="nx">cta</span><span class="p">?.</span><span class="nx">data</span><span class="p">?.</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">notification</span><span class="p">.</span><span class="nx">cta</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">url</span><span class="p">;</span> <span class="p">}</span> <span class="p">},</span> <span class="p">[]);</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">NotificationCenter</span> <span class="nx">colorScheme</span><span class="o">=</span><span class="dl">"</span><span class="s2">dark</span><span class="dl">"</span> <span class="nx">onNotificationClick</span><span class="o">=</span><span class="p">{</span><span class="nx">onNotificationClick</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">};</span> <span class="k">export</span> <span class="k">default</span> <span class="nf">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nb">Promise</span><span class="p">.</span><span class="nf">resolve</span><span class="p">(</span><span class="nx">Notifications</span><span class="p">),</span> <span class="p">{</span> <span class="na">ssr</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <p>The code above does the following:</p> <ul> <li>It imports the original <code>&lt;NotificationCenter /&gt;</code> component from Novu.</li> <li>It uses Next’s dynamic functionality that allows us to dynamically load a component on the client side, and to use the <code>ssr</code> option to disable server-rendering.</li> <li>It defines a notificationClick handler for the onNotificationClick prop of the NotificationCenter component (The function that is called when the notification item is clicked).</li> <li>The <code>colorScheme</code> is one of the props you can pass to the NotificationCenter component. It takes in either “light” or “dark” values to enable you set the UI mode.</li> </ul> <p>Finally, head back to the <code>pages/index.js</code> file, import our newly created component and use it in the <code>NovuProvider</code> parent component like so:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="p">...</span> <span class="k">import</span> <span class="nx">Notification</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../components/notifications</span><span class="dl">"</span><span class="p">;</span> <span class="p">...</span> <span class="p">...</span> <span class="p">...</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">NovuProvider</span> <span class="nx">subscriberId</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">}</span> <span class="nx">applicationIdentifier</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_APP_ID</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">Notification</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/NovuProvider</span><span class="err">&gt; </span><span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span><span class="p">...</span> <span class="p">...</span> <span class="p">...</span> </code></pre> </div> <p>Lastly, let's adjust the components' position to place the Notification Center in the middle of the page.</p> <p><em>pages/index.js</em><br> </p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code><span class="k">import</span> <span class="nx">Image</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/image</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Inter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/font/google</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">NovuProvider</span><span class="p">,</span> <span class="nx">PopoverNotificationCenter</span><span class="p">,</span> <span class="nx">NotificationBell</span><span class="p">,</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/notification-center</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">Notification</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../components/notifications</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">inter</span> <span class="o">=</span> <span class="nc">Inter</span><span class="p">({</span> <span class="na">subsets</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">latin</span><span class="dl">"</span><span class="p">]</span> <span class="p">});</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">Home</span><span class="p">()</span> <span class="p">{</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">main</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`flex min-h-screen flex-col items-center justify-between p-24 </span><span class="p">${</span><span class="nx">inter</span><span class="p">.</span><span class="nx">className</span><span class="p">}</span><span class="s2">`</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30</span><span class="dl">"</span><span class="o">&gt;</span> <span class="nx">Dev</span><span class="p">.</span><span class="nx">to</span> <span class="nx">In</span><span class="o">-</span><span class="nx">App</span> <span class="nx">Notifications</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700/10 after:dark:from-sky-900 after:dark:via-[#0141ff]/40 before:lg:h-[360px]</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">NovuProvider</span> <span class="nx">subscriberId</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">}</span> <span class="nx">applicationIdentifier</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_APP_ID</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">Notification</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/NovuProvider</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://nextjs.org/docs?utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Docs</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Find</span> <span class="k">in</span><span class="o">-</span><span class="nx">depth</span> <span class="nx">information</span> <span class="nx">about</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span> <span class="nx">features</span> <span class="nx">and</span> <span class="nx">API</span><span class="p">.</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://nextjs.org/learn?utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Learn</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Learn</span> <span class="nx">about</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span> <span class="k">in</span> <span class="nx">an</span> <span class="nx">interactive</span> <span class="nx">course</span> <span class="kd">with</span><span class="o">&amp;</span><span class="nx">nbsp</span><span class="p">;</span><span class="nx">quizzes</span><span class="o">!</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://vercel.com/templates?framework=next.js&amp;utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Templates</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Discover</span> <span class="nx">and</span> <span class="nx">deploy</span> <span class="nx">boilerplate</span> <span class="nx">example</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span><span class="o">&amp;</span><span class="nx">nbsp</span><span class="p">;</span><span class="nx">projects</span><span class="p">.</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://vercel.com/new?utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Deploy</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50 text-balance`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Instantly</span> <span class="nx">deploy</span> <span class="nx">your</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span> <span class="nx">site</span> <span class="nx">to</span> <span class="nx">a</span> <span class="nx">shareable</span> <span class="nx">URL</span> <span class="kd">with</span> <span class="nx">Vercel</span><span class="p">.</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/main</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">}</span> </code></pre> </div> <p><strong>Note:</strong> Ensure this is the full code of the <code>pages/index.js</code> file to avoid missing anything.</p> <p>Now, reload your app. It should resemble <a href="proxy.php?url=http://dev.to/">Dev.to</a>'s centered Notification page. Yaayyyy!!</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-23-at-10.30.02%402x-1024x666.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-23-at-10.30.02%402x-1024x666.png" alt="Centered Notifications"></a>Publish a new article on <a href="proxy.php?url=http://dev.to/">Dev.to</a> and observe how the notifications arrive via the centralized Notifications component, even without a notification bell. The Notification unseen counter shows right there.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-23-at-10.33.18%402x-1024x693.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-23-at-10.33.18%402x-1024x693.png" alt="Change to light mode"></a>Go one step further, make the background light mode &amp; change the Notification Center to light mode.</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-23-at-10.37.22%402x-1024x685.png" alt="Email Community Digest">Up Next: Build <a href="proxy.php?url=http://Dev.top" rel="noopener noreferrer">Dev.to</a> Email Community Digest </h2> <p>You've learned how to develop an In-App Notification System in under 30 minutes. Over time, I've learned that the best developers use battle-tested tools to build their apps and systems.</p> <p>Fortunately, there's more. I have additional tips to share with you. 😉</p> <p>Next, I'll demonstrate how to use Novu to build an Email Community Digest, similar to the excellent digest from <a href="proxy.php?url=http://dev.to/">Dev.to</a>, which keeps us updated on the top published articles.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-15.14.13%402x-1024x648.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmanage.novu.co%2Fwp-content%2Fuploads%2F2024%2F04%2FCleanShot-2024-04-22-at-15.14.13%402x-1024x648.png" alt="Stay tuned."></a>Stay tuned for the next part of this article!</p> <p>A reminder to check out the <a href="proxy.php?url=https://github.com/novuhq/novu-devto" rel="noopener noreferrer">completed code for this article</a> on GitHub. Let me know if you run into any issues at all.</p> <p>Don't hesitate to ask any questions or request support. You can find me on <a href="proxy.php?url=https://bit.ly/3QfOQHp" rel="noopener noreferrer">Discord</a> and <a href="proxy.php?url=https://bit.ly/3JCpHCR" rel="noopener noreferrer">Twitter</a>. Please feel free to reach out.</p> webdev javascript notifications devto Building an Investor List App with Novu and Supabase Prosper Otemuyiwa Mon, 22 Apr 2024 09:22:46 +0000 https://dev.to/novu/building-an-investor-list-app-with-novu-and-supabase-50nj https://dev.to/novu/building-an-investor-list-app-with-novu-and-supabase-50nj <p>Supabase is an impressive open-source alternative to Firebase. It provides multiple tools that enable you to build comprehensive backend systems, including features for authentication, databases, storage, and real-time updates.</p> <p>In this article, I'll guide you through the process of integrating your Next.js Supabase app with Novu. This integration will allow for a seamless, quick, and stress-free notification experience. Novu is an open-source notification infrastructure designed to assist engineering teams in creating robust product notification experiences.</p> <p>We'll create an <strong>AI investor list app</strong>. This app will enable users to add details of angel investors interested in funding AI startups at the Pre-seed, Seed, and Series A stages. It will also announce and send in-app notifications when new investors are added.</p> <h2> <strong>Prerequisites</strong> </h2> <p>Before diving into the article, make sure you have the following:</p> <ul> <li>Node.js installed on your development machine.</li> <li>A Novu account. If you don’t have one, sign up for free at <strong><a href="proxy.php?url=https://web.novu.co/" rel="noopener noreferrer">the web dashboard</a>.</strong> </li> <li>A Supabase account. If you don’t have one, sign up for <a href="proxy.php?url=https://supabase.com/dashboard/projects" rel="noopener noreferrer">free at the dashboard</a>.</li> </ul> <p>If you want to explore the code right away, you can <strong><a href="proxy.php?url=https://github.com/novuhq/novu-supabase-app" rel="noopener noreferrer">view the completed code</a></strong> <a href="proxy.php?url=https://github.com/novuhq/react-translation-app" rel="noopener noreferrer"></a> on GitHub.</p> <h2> Set up a Next.js App </h2> <p>To create a Next.js app, open your terminal, <code>cd</code> into the directory you’d like to create the app in, and run the following command:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> npx create-next-app@latest supabase-novu </code></pre> </div> <p>Go through the prompts:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ytjkv7e1da1fxue944p.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ytjkv7e1da1fxue944p.png" alt="Prompts"></a></p> <p><code>cd</code> into the directory, <code>supabase-novu</code> and run to start the app in your browser:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> npm run dev </code></pre> </div> <h2> Set up Supabase in the App </h2> <p>Run the following command to add Supabase to the project:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> npm i @supabase/supabase-js </code></pre> </div> <p>Ensure you have signed up and <strong>created a new project</strong> on Supabase. It takes a couple of minutes to have your project instance set up on the dashboard. Once done, it should look similar to the image below:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favke3svqiape36odicel.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favke3svqiape36odicel.png" alt="Database created"></a></p> <p>We’ll go ahead to create a database table, <strong>investors</strong>, in our newly created project. Click on the <strong>SQL Editor</strong> on the dashboard and add the following SQL query to the editor.</p> <div class="highlight js-code-highlight"> <pre class="highlight sql"><code> <span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">investors</span> <span class="p">(</span> <span class="n">id</span> <span class="nb">bigint</span> <span class="k">generated</span> <span class="k">by</span> <span class="k">default</span> <span class="k">as</span> <span class="k">identity</span> <span class="k">primary</span> <span class="k">key</span><span class="p">,</span> <span class="n">email_reach</span> <span class="nb">text</span> <span class="k">unique</span><span class="p">,</span> <span class="n">name</span> <span class="nb">text</span><span class="p">,</span> <span class="n">website</span> <span class="nb">text</span><span class="p">,</span> <span class="n">funding_amount</span> <span class="nb">text</span><span class="p">,</span> <span class="n">funding_type</span> <span class="nb">text</span><span class="p">,</span> <span class="n">inserted_at</span> <span class="nb">timestamp</span> <span class="k">with</span> <span class="nb">time</span> <span class="k">zone</span> <span class="k">default</span> <span class="n">timezone</span><span class="p">(</span><span class="s1">'utc'</span><span class="p">::</span><span class="nb">text</span><span class="p">,</span> <span class="n">now</span><span class="p">())</span> <span class="k">not</span> <span class="k">null</span> <span class="p">);</span> <span class="k">alter</span> <span class="k">table</span> <span class="n">investors</span> <span class="n">enable</span> <span class="k">row</span> <span class="k">level</span> <span class="k">security</span><span class="p">;</span> <span class="k">create</span> <span class="n">policy</span> <span class="nv">"Investors are public."</span> <span class="k">on</span> <span class="n">investors</span> <span class="k">for</span> <span class="k">select</span> <span class="k">using</span> <span class="p">(</span><span class="k">true</span><span class="p">);</span> </code></pre> </div> <p>It should look like so:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkumg89w581ehuzql60he.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkumg89w581ehuzql60he.png" alt="Alter table"></a></p> <p>Click on <strong>“Run”</strong> to run the query, create the table and apply a Policy.</p> <p>This will go ahead to create an <code>investors</code> table. It also enables <strong>row level permission</strong> that allows anyone to query for all investors. In this article we are not concerned with authentication. However, this is something you should consider if you are to make it a production grade app.</p> <p>You should be able to see your table created successfully!</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funq3r696txojzk2qzre3.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funq3r696txojzk2qzre3.png" alt="Table created"></a><br> Next, we need to connect our Next.js app with our Supabase backend.</p> <h2> Connect Supabase with Next.js </h2> <p>Create a <code>.env.local</code> or <code>.env</code> file in the root of the Next.js app and add the following environment variables:</p> <div class="highlight js-code-highlight"> <pre class="highlight sql"><code> <span class="n">NEXT_PUBLIC_SUPABASE_URL</span><span class="o">=&lt;</span><span class="k">insert</span><span class="o">-</span><span class="n">supabase</span><span class="o">-</span><span class="n">db</span><span class="o">-</span><span class="n">url</span><span class="o">&gt;</span> <span class="n">NEXT_PUBLIC_SUPABASE_ANON_KEY</span><span class="o">=&lt;</span><span class="k">insert</span><span class="o">-</span><span class="n">anon</span><span class="o">-</span><span class="k">key</span><span class="o">&gt;</span> </code></pre> </div> <p>Please replace the placeholders here with the correct values from your dashboard.</p> <p>Click on the <strong>“Project Settings icon”</strong> at the bottom left and click on <strong>“API”</strong> to show you the screen below to obtain the correct values.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8mnhre0ba57y7n3viee.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8mnhre0ba57y7n3viee.png" alt="Project settings"></a><br> Create an <code>utils</code> folder inside the <code>src</code> directory of our Next.js project. Then go ahead to create a <code>supabase.js</code> file in it. Add the code below:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">import</span> <span class="p">{</span> <span class="nx">createClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@supabase/supabase-js</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">supabaseUrl</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUPABASE_URL</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">supabaseKey</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUPABASE_ANON_KEY</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">supabase</span> <span class="o">=</span> <span class="nf">createClient</span><span class="p">(</span><span class="nx">supabaseUrl</span><span class="p">,</span> <span class="nx">supabaseKey</span><span class="p">);</span> </code></pre> </div> <p>This is the Supabase client that we can use anywhere in our app going forward. The <code>createClient</code> function that we imported from the supabase library makes it possible.</p> <p>Next, let’s set up our UI components and wire them to the Supabase backend.</p> <h2> Build UI to Interact with Supabase </h2> <p>Open up the <code>src/pages/index.js</code> file. We will modify it a lot.</p> <p>Replace the content of the file with the code below:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">import</span> <span class="nx">Image</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/image</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Inter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/font/google</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">supabase</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/utils/supabase</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useEffect</span><span class="p">,</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">inter</span> <span class="o">=</span> <span class="nc">Inter</span><span class="p">({</span> <span class="na">subsets</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">latin</span><span class="dl">"</span><span class="p">]</span> <span class="p">});</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">Home</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">investors</span><span class="p">,</span> <span class="nx">setInvestors</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">([]);</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">loading</span><span class="p">,</span> <span class="nx">setLoading</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span> <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nf">fetchAllInvestors</span><span class="p">();</span> <span class="p">},</span> <span class="p">[]);</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">fetchAllInvestors</span><span class="p">()</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">supabase</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="dl">"</span><span class="s2">investors</span><span class="dl">"</span><span class="p">).</span><span class="nf">select</span><span class="p">(</span><span class="dl">"</span><span class="s2">*</span><span class="dl">"</span><span class="p">);</span> <span class="k">if </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span> <span class="nf">setInvestors</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="nf">alert</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span> <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">addInvestor</span><span class="p">(</span> <span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="nx">funding_type</span> <span class="p">)</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">supabase</span> <span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="dl">"</span><span class="s2">investors</span><span class="dl">"</span><span class="p">)</span> <span class="p">.</span><span class="nf">insert</span><span class="p">([</span> <span class="p">{</span> <span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="nx">funding_type</span><span class="p">,</span> <span class="p">},</span> <span class="p">])</span> <span class="p">.</span><span class="nf">single</span><span class="p">();</span> <span class="k">if </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span> <span class="nf">alert</span><span class="p">(</span><span class="dl">"</span><span class="s2">Investor added successfully</span><span class="dl">"</span><span class="p">);</span> <span class="nf">fetchAllInvestors</span><span class="p">();</span> <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="nf">alert</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">function</span> <span class="nf">handleSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">e</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">email_reach</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">email_reach</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">website</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">website</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">funding_amount</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">funding_amount</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">funding_type</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">funding_type</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="nf">addInvestor</span><span class="p">(</span><span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="nx">funding_type</span><span class="p">);</span> <span class="p">}</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">main</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`flex min-h-screen flex-col items-center justify-between p-24 </span><span class="p">${</span><span class="nx">inter</span><span class="p">.</span><span class="nx">className</span><span class="p">}</span><span class="s2">`</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30</span><span class="dl">"</span><span class="o">&gt;</span> <span class="nx">NOVU</span> <span class="nx">SUPABASE</span> <span class="nx">DASHBOARD</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0</span><span class="dl">"</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://vercel.com?utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="nx">By</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">Image</span> <span class="nx">src</span><span class="o">=</span><span class="dl">"</span><span class="s2">/vercel.svg</span><span class="dl">"</span> <span class="nx">alt</span><span class="o">=</span><span class="dl">"</span><span class="s2">Vercel Logo</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">dark:invert</span><span class="dl">"</span> <span class="nx">width</span><span class="o">=</span><span class="p">{</span><span class="mi">100</span><span class="p">}</span> <span class="nx">height</span><span class="o">=</span><span class="p">{</span><span class="mi">24</span><span class="p">}</span> <span class="nx">priority</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="kd">class</span><span class="o">=</span><span class="dl">"</span><span class="s2">grid grid-cols-2</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-1 flex justify-center</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">form</span> <span class="nx">onSubmit</span><span class="o">=</span><span class="p">{</span><span class="nx">handleSubmit</span><span class="p">}</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">label</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">p-3 block</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Name</span><span class="p">:</span><span class="o">&lt;</span><span class="sr">/label</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">input</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-black p-2</span><span class="dl">"</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text</span><span class="dl">"</span> <span class="nx">name</span><span class="o">=</span><span class="dl">"</span><span class="s2">name</span><span class="dl">"</span> <span class="nx">required</span> <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">Enter name</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">label</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">p-3 block</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Email</span> <span class="nx">Reach</span><span class="p">:</span><span class="o">&lt;</span><span class="sr">/label</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">input</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-black p-2</span><span class="dl">"</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text</span><span class="dl">"</span> <span class="nx">name</span><span class="o">=</span><span class="dl">"</span><span class="s2">email_reach</span><span class="dl">"</span> <span class="nx">required</span> <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">Enter investor email</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-5</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">label</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">p-2 block</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Website</span><span class="p">:</span><span class="o">&lt;</span><span class="sr">/label</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">input</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-black p-2</span><span class="dl">"</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text</span><span class="dl">"</span> <span class="nx">name</span><span class="o">=</span><span class="dl">"</span><span class="s2">website</span><span class="dl">"</span> <span class="nx">required</span> <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">Enter website</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-5</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">label</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">p-2 block</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Funding</span> <span class="nc">Amount </span><span class="p">(</span><span class="nx">Up</span> <span class="nx">to</span> <span class="nx">X</span> <span class="nx">USD</span><span class="p">):</span><span class="o">&lt;</span><span class="sr">/label</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">input</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-black p-2</span><span class="dl">"</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text</span><span class="dl">"</span> <span class="nx">name</span><span class="o">=</span><span class="dl">"</span><span class="s2">funding_amount</span><span class="dl">"</span> <span class="nx">required</span> <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">Enter funding amount</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-5</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">label</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">p-2 block</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Funding</span> <span class="nx">Type</span><span class="p">:</span><span class="o">&lt;</span><span class="sr">/label</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">input</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-black p-2</span><span class="dl">"</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text</span><span class="dl">"</span> <span class="nx">name</span><span class="o">=</span><span class="dl">"</span><span class="s2">funding_type</span><span class="dl">"</span> <span class="nx">required</span> <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">Enter type of Funding</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">button</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">submit</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">bg-blue-600 p-2 rounded-md mt-5 px-12</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="nx">Submit</span> <span class="nx">Investor</span> <span class="nx">Details</span> <span class="o">&lt;</span><span class="sr">/button</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/form</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-1 flex justify-center</span><span class="dl">"</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">investors</span><span class="p">?.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span> <span class="p">?</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="nx">There</span> <span class="nx">are</span> <span class="nx">no</span> <span class="nx">investors</span> <span class="nx">yet</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">:</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mb-5</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Here</span> <span class="nx">are</span> <span class="nx">the</span> <span class="nx">investors</span> <span class="na">available</span><span class="p">:</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">table</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">thead</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">tr</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">th</span><span class="o">&gt;</span><span class="nx">Name</span> <span class="o">&lt;</span><span class="sr">/th</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">th</span><span class="o">&gt;</span><span class="nx">Email</span> <span class="nx">Reach</span><span class="o">&lt;</span><span class="sr">/th</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">th</span><span class="o">&gt;</span><span class="nx">Website</span><span class="o">&lt;</span><span class="sr">/th</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">th</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">p-3</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Funding</span> <span class="nx">Amt</span><span class="o">&lt;</span><span class="sr">/th</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">th</span><span class="o">&gt;</span><span class="nx">Funding</span> <span class="nx">Type</span> <span class="o">&lt;</span><span class="sr">/th</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/tr</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/thead</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">tbody</span> <span class="nx">className</span><span class="o">=</span><span class="dl">""</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">investors</span><span class="p">?.</span><span class="nf">map</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">tr</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">td</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/td</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">td</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">email_reach</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/td</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">td</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">website</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/td</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">td</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">funding_amount</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/td</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">td</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">funding_type</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/td</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/tr</span><span class="err">&gt; </span> <span class="p">))}</span> <span class="o">&lt;</span><span class="sr">/tbody</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/table</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)}</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://nextjs.org/docs?utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Docs</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Find</span> <span class="k">in</span><span class="o">-</span><span class="nx">depth</span> <span class="nx">information</span> <span class="nx">about</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span> <span class="nx">features</span> <span class="nx">and</span> <span class="nx">API</span><span class="p">.</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://nextjs.org/learn?utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Learn</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Learn</span> <span class="nx">about</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span> <span class="k">in</span> <span class="nx">an</span> <span class="nx">interactive</span> <span class="nx">course</span> <span class="kd">with</span><span class="o">&amp;</span><span class="nx">nbsp</span><span class="p">;</span><span class="nx">quizzes</span><span class="o">!</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://vercel.com/templates?framework=next.js&amp;utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Templates</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Discover</span> <span class="nx">and</span> <span class="nx">deploy</span> <span class="nx">boilerplate</span> <span class="nx">example</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span><span class="o">&amp;</span><span class="nx">nbsp</span><span class="p">;</span><span class="nx">projects</span><span class="p">.</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://vercel.com/new?utm_source=create-next-app&amp;utm_medium=default-template-tw&amp;utm_campaign=create-next-app&gt;</span><span class="dl">"</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30</span><span class="dl">"</span> <span class="nx">target</span><span class="o">=</span><span class="dl">"</span><span class="s2">_blank</span><span class="dl">"</span> <span class="nx">rel</span><span class="o">=</span><span class="dl">"</span><span class="s2">noopener noreferrer</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`mb-3 text-2xl font-semibold`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Deploy</span><span class="p">{</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">-&amp;</span><span class="nx">gt</span><span class="p">;</span> <span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`m-0 max-w-[30ch] text-sm opacity-50 text-balance`</span><span class="p">}</span><span class="o">&gt;</span> <span class="nx">Instantly</span> <span class="nx">deploy</span> <span class="nx">your</span> <span class="nx">Next</span><span class="p">.</span><span class="nx">js</span> <span class="nx">site</span> <span class="nx">to</span> <span class="nx">a</span> <span class="nx">shareable</span> <span class="nx">URL</span> <span class="kd">with</span> <span class="nx">Vercel</span><span class="p">.</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/a</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/main</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">}</span> </code></pre> </div> <p>Let’s go over some sections of this code block:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="kd">function</span> <span class="nf">handleSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">e</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">email_reach</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">email_reach</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">website</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">website</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">funding_amount</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">funding_amount</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">funding_type</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">funding_type</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="nf">addInvestor</span><span class="p">(</span><span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="nx">funding_type</span><span class="p">);</span> <span class="p">}</span> </code></pre> </div> <p>This function handles the submission of the form. It grabs the value from the form fields and passes it to the <code>&lt;span style="background-color: initial; font-family: inherit; font-size: inherit; color: initial;"&gt;addInvestor&lt;/span&gt;</code> function.</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">async</span> <span class="kd">function</span> <span class="nf">addInvestor</span><span class="p">(</span> <span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="nx">funding_type</span> <span class="p">)</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">supabase</span> <span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="dl">"</span><span class="s2">investors</span><span class="dl">"</span><span class="p">)</span> <span class="p">.</span><span class="nf">insert</span><span class="p">([</span> <span class="p">{</span> <span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="nx">funding_type</span><span class="p">,</span> <span class="p">},</span> <span class="p">])</span> <span class="p">.</span><span class="nf">single</span><span class="p">();</span> <span class="k">if </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span> <span class="nf">alert</span><span class="p">(</span><span class="dl">"</span><span class="s2">Investor added successfully</span><span class="dl">"</span><span class="p">);</span> <span class="nf">fetchAllInvestors</span><span class="p">();</span> <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="nf">alert</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>The <code>addInvestor</code> function takes in the right arguments and makes a query to supabase to insert the values into the <code>investors</code> table. This function also calls the <code>fetchAllInvestors()</code> function that retrieves all data from the <code>investors</code> table.</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">async</span> <span class="kd">function</span> <span class="nf">fetchAllInvestors</span><span class="p">()</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">supabase</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="dl">"</span><span class="s2">investors</span><span class="dl">"</span><span class="p">).</span><span class="nf">select</span><span class="p">(</span><span class="dl">"</span><span class="s2">*</span><span class="dl">"</span><span class="p">);</span> <span class="k">if </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span> <span class="nf">setInvestors</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="nf">alert</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span> <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>As you can see from the code above, the <code>fetchAllInvestors()</code> function makes a SELECT query to the <code>investors</code> table to return and all the investors to the frontend.</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="kd">const</span> <span class="p">[</span><span class="nx">investors</span><span class="p">,</span> <span class="nx">setInvestors</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">([]);</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">loading</span><span class="p">,</span> <span class="nx">setLoading</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span> <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nf">fetchAllInvestors</span><span class="p">();</span> <span class="p">},</span> <span class="p">[]);</span> </code></pre> </div> <p>When the page loads up, it calls the <code>fetchAllInvestors()</code> function by default.</p> <p>Run your app with <code>npm run dev</code>. It should look like this:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpnbixfv09d88ldubp2m.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpnbixfv09d88ldubp2m.png" alt="Run app"></a><br> Add some investor details and submit. Your app should look like this now:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspe11ln9r0yh3fr1op0x.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspe11ln9r0yh3fr1op0x.png" alt="Add items"></a><br> As we add items, they are saved to the Supabase database and displayed on the screen.</p> <p>Inspect the table on Supabase. The data should appear as follows:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9olafsn0szamyzwqteai.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9olafsn0szamyzwqteai.png" alt="Inspect table"></a><br> Next, we'll set up notifications in the app. Our goal is to alert everyone when a new investor is added to the database. This is where Novu comes into play!</p> <h2> Set up Novu in the App </h2> <p>Run the following command to install the Novu node SDK:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="nx">npm</span> <span class="nx">install</span> <span class="p">@</span><span class="nd">novu</span><span class="sr">/nod</span><span class="err">e </span> </code></pre> </div> <p>Run the following command to install the Novu Notification Center package:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="nx">npm</span> <span class="nx">install</span> <span class="p">@</span><span class="nd">novu</span><span class="sr">/notification-cente</span><span class="err">r </span> </code></pre> </div> <p>The Novu Notification Center package provides a React component library that adds a notification center to your React app. The <a href="proxy.php?url=https://docs.novu.co/notification-center/introduction#ui-libraries" rel="noopener noreferrer">package is also available for non-React apps</a>.</p> <p>Before we can start sending and receiving notifications, we need to set up a few things:</p> <ol> <li>Create a workflow for sending notifications,</li> <li>Create a subscriber - recipient of notifications.</li> </ol> <h2> <strong><a href="proxy.php?url=https://docs.novu.co/quickstarts/react#create-a-workflow" rel="noopener noreferrer"></a>Create a Novu Workflow</strong> </h2> <p>A workflow is a blueprint for notifications. It includes the following:</p> <ul> <li>Workflow name and Identifier</li> <li>Channels: - Email, SMS, Chat, In-App and Push</li> </ul> <p>Follow the steps below to create a workflow:</p> <ol> <li>Click <strong>Workflow</strong> on the left sidebar of your Novu dashboard.</li> <li>Click the <strong>Add a Workflow</strong> button on the top left. You can select a Blank workflow or use one of the existing templates.</li> <li>The name of the new workflow is currently <strong>“Untitled”</strong>. Rename it to <code>notify users of new investors</code>.</li> <li>Select <strong>In-App</strong> as the channel you want to add. </li> </ol> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fu5iwb3wyyr6n7juylu.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fu5iwb3wyyr6n7juylu.png" alt="Select In-App"></a></p> <ol> <li>Click on the recently added <strong>“In-App”</strong> channel and add the following text to it. Once you’re done, click <strong>“Update”</strong> to save your configuration.</li> </ol> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx7w1dorik7ywcgndocx.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx7w1dorik7ywcgndocx.png" alt="Payload variables"></a><br> The <code>{{name}},{{funding_amount}}</code> and <code>{{email}}</code> are custom variables. This means that we can pass them to our <a href="proxy.php?url=https://docs.novu.co/quickstarts/general#pass-the-subscriber-information-in-the-trigger-quickest" rel="noopener noreferrer">payload</a> before we trigger a notification. You’ll see this when we create the API route to send notifications.</p> <h2> <strong><a href="proxy.php?url=https://docs.novu.co/quickstarts/react#create-a-subscriber" rel="noopener noreferrer"></a>Create a subscriber</strong> </h2> <p>If you click <strong>“Subscriber”</strong> on the left sidebar of the <strong><a href="proxy.php?url=https://web.novu.co/subscribers" rel="noopener noreferrer">Novu dashboard</a></strong>, you’ll see the subscriber list. As a first time Novu user, it will be an empty list. Subscribers are your <strong>app users.</strong></p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmji1osf9smwl1dcz4fnn.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmji1osf9smwl1dcz4fnn.png" alt="Subscribers"></a><br> Open your terminal and run the following script to create a subscriber:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> curl <span class="nt">--location</span> <span class="s1">'&lt;https://api.novu.co/v1/subscribers&gt;'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Accept: application/json'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Authorization: ApiKey &lt;NOVU_API_KEY&gt;'</span> <span class="se">\</span> <span class="nt">--data-raw</span> <span class="s1">'{ "firstName": "John", "lastName": "Doe", "email": "[email protected]", "phone": "+1234567890" }'</span> </code></pre> </div> <p><strong>Note:</strong> You can get your NOVU API Key from the <a href="proxy.php?url=https://web.novu.co/settings" rel="noopener noreferrer">settings section</a> of your Novu dashboard.</p> <p>Refresh the Subscribers page on your Novu dashboard. You should see the recently added subscriber now! You can also add a subscriber to Novu by running this <a href="proxy.php?url=https://docs.novu.co/api-reference/subscribers/create-subscriber" rel="noopener noreferrer">API endpoint</a>.</p> <p>The best option to add a subscriber is via code in your backend. With Node.js code, you can run the following code to create a subscriber:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">import</span> <span class="p">{</span> <span class="nx">Novu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/node</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// Insert your Novu API Key here</span> <span class="kd">const</span> <span class="nx">novu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Novu</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;NOVU_API_KEY&gt;</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// Create a subscriber on Novu</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nx">subscribers</span><span class="p">.</span><span class="nf">identify</span><span class="p">(</span><span class="dl">"</span><span class="s2">132</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">email</span><span class="p">:</span> <span class="dl">"</span><span class="s2">[email protected]</span><span class="dl">"</span><span class="p">,</span> <span class="na">firstName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">John</span><span class="dl">"</span><span class="p">,</span> <span class="na">lastName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Doe</span><span class="dl">"</span><span class="p">,</span> <span class="na">phone</span><span class="p">:</span> <span class="dl">"</span><span class="s2">+13603963366</span><span class="dl">"</span><span class="p">,</span> <span class="p">});</span> </code></pre> </div> <h2> <strong>Set up Novu Notification Center in the App</strong> </h2> <p>Head over to <code>scr/pages/index.js</code>. We’ll modify this page again to include the following:</p> <ul> <li>Import and display the Novu Notification Center.</li> <li>A function to trigger the notification when a new investor is added.</li> </ul> <p>Import the Notification Center components from the Novu notification center package like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="p">...</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">NovuProvider</span><span class="p">,</span> <span class="nx">PopoverNotificationCenter</span><span class="p">,</span> <span class="nx">NotificationBell</span><span class="p">,</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/notification-center</span><span class="dl">"</span><span class="p">;</span> </code></pre> </div> <p>Display the notification center by adding the imported components like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="p">...</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">main</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`flex min-h-screen flex-col items-center justify-between p-24 </span><span class="p">${</span><span class="nx">inter</span><span class="p">.</span><span class="nx">className</span><span class="p">}</span><span class="s2">`</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30</span><span class="dl">"</span><span class="o">&gt;</span> <span class="nx">NOVU</span> <span class="nx">SUPABASE</span> <span class="nx">DASHBOARD</span> <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">NovuProvider</span> <span class="nx">subscriberId</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">}</span> <span class="nx">applicationIdentifier</span><span class="o">=</span><span class="p">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_APP_ID</span><span class="p">}</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">PopoverNotificationCenter</span><span class="o">&gt;</span> <span class="p">{({</span> <span class="nx">unseenCount</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">NotificationBell</span> <span class="nx">unseenCount</span><span class="o">=</span><span class="p">{</span><span class="nx">unseenCount</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span> <span class="p">)}</span> <span class="o">&lt;</span><span class="sr">/PopoverNotificationCenter</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/NovuProvider</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">...</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">grid grid-cols-2</span><span class="dl">"</span><span class="o">&gt;</span> </code></pre> </div> <p>Now, your app should display a notification bell. When this bell is clicked, a notification user interface will pop up.</p> <p><strong>Note:</strong> The <a href="proxy.php?url=https://docs.novu.co/notification-center/client/react/api-reference#novuprovider" rel="noopener noreferrer">NovuProvider root Component ships with many props</a> that can be used to customize the Notification Center to your taste. The floating <a href="proxy.php?url=https://docs.novu.co/notification-center/client/react/api-reference#popovernotificationcenter" rel="noopener noreferrer">popover component</a> that appears when clicking on the <strong><a href="proxy.php?url=https://docs.novu.co/notification-center/client/react/api-reference#notificationbell" rel="noopener noreferrer">NotificationBell</a></strong> button. It renders the <strong><a href="proxy.php?url=https://docs.novu.co/notification-center/client/react/api-reference#notificationcenter" rel="noopener noreferrer">NotificationCenter</a></strong> component inside its content.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdgy2fs76t5e4m3ta0t7x.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdgy2fs76t5e4m3ta0t7x.png" alt="Notification Center"></a><br> Add the following Novu env variables to your <code>.env.local</code> file:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="o">=</span> <span class="nx">NEXT_PUBLIC_NOVU_APP_ID</span><span class="o">=</span> <span class="nx">NEXT_PUBLIC_NOVU_API_KEY</span><span class="o">=</span> </code></pre> </div> <p>The <strong>Novu API Key</strong> and <strong>APP ID</strong> can be found in the <a href="proxy.php?url=https://web.novu.co/settings" rel="noopener noreferrer">Settings</a> section of your Novu dashboard.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewiecizgy4de1cchx3da.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewiecizgy4de1cchx3da.png" alt="Novu credentials"></a><br> The Subscriber ID too is on your dashboard. Earlier in this article, we created a subscriber. We need that ID now.</p> <p><strong>Note:</strong> Adding the Subscriber ID to the env file is only for the purpose of this article. Your subscriber ID is the user ID obtained from an authenticated user. When a user logs in, their ID should be the value passed to the <strong>subscriberId</strong> property of the <strong>NovuProvider</strong> component.</p> <h2> Develop a Simple API to Trigger Notifications </h2> <p>Create a <code>send-notification.js</code> file in the <code>src/pages/api</code> directory. Add the code below to it:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">import</span> <span class="p">{</span> <span class="nx">Novu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/node</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">novu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Novu</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_API_KEY</span><span class="p">);</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">workflowTriggerID</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">notify-users-of-new-investors</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">handler</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">email</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">amount</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nf">trigger</span><span class="p">(</span><span class="nx">workflowTriggerID</span><span class="p">,</span> <span class="p">{</span> <span class="na">to</span><span class="p">:</span> <span class="p">{</span> <span class="na">subscriberId</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">,</span> <span class="p">},</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span> <span class="na">funding_amount</span><span class="p">:</span> <span class="nx">amount</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="nx">email</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">({</span> <span class="na">finish</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>The <code>workflowTriggerID</code> value is obtained from the workflow dashboard. Earlier, when we set up a workflow titled <code>notify users of new investors</code>, Novu created a slug from this title to serve as the trigger ID.</p> <p>You can see it here:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7btbqymytzzeraw42kqs.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7btbqymytzzeraw42kqs.png" alt="Workflow Trigger ID"></a><br> The block of code below triggers the notification via the Novu SDK:</p> <ul> <li>Accepts the workflow trigger ID to determine which workflow to trigger.</li> <li>Accepts the subscriber ID value to identify the notification recipient.</li> <li>Accepts a payload object that represents the parameters to inject into the workflow variables.</li> </ul> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nf">trigger</span><span class="p">(</span><span class="nx">workflowTriggerID</span><span class="p">,</span> <span class="p">{</span> <span class="na">to</span><span class="p">:</span> <span class="p">{</span> <span class="na">subscriberId</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_SUBSCRIBER_ID</span><span class="p">,</span> <span class="p">},</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span> <span class="na">funding_amount</span><span class="p">:</span> <span class="nx">amount</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="nx">email</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> </code></pre> </div> <p>Next, we need to set up one more thing on the frontend before we test the notification experience in our app.</p> <h2> Add a Notification Function to Index.js </h2> <p>Create a function inside the <code>Home()</code> function to call our recently created API like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">async</span> <span class="kd">function</span> <span class="nf">triggerNotification</span><span class="p">(</span><span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">)</span> <span class="p">{</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/api/send-notification</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="na">email</span><span class="p">:</span> <span class="nx">email_reach</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span> <span class="na">amount</span><span class="p">:</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="p">}),</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>Call the <code>triggerNotification</code> function just after the <code>addInvestor</code> function within the <code>handleSubmit</code> function.</p> <p>Your <code>handleSubmit</code> function should now appear as follows:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="kd">function</span> <span class="nf">handleSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">e</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">email_reach</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">email_reach</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">website</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">website</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">funding_amount</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">funding_amount</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">funding_type</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">funding_type</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> <span class="nf">addInvestor</span><span class="p">(</span><span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">website</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">,</span> <span class="nx">funding_type</span><span class="p">);</span> <span class="nf">triggerNotification</span><span class="p">(</span><span class="nx">email_reach</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">funding_amount</span><span class="p">);</span> <span class="p">}</span> </code></pre> </div> <p>Run your app again and submit a new investor detail. You should get an instant In-App notification with the details of the new investor.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jy3q0q2n9glw2ihsqmt.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jy3q0q2n9glw2ihsqmt.png" alt="Step 1"></a></p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn4jxerpo1vcm39b071w.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn4jxerpo1vcm39b071w.png" alt="Step 2"></a></p> <h2> A Step Further - Notify All Investors </h2> <p>We currently have a database containing investor emails. This is valuable data. Let's consider adding a feature to email these investors about new investment rounds from startups seeking funding.</p> <p>We will not add a UI for the save of brevity. You can add a UI as an improvement and challenge.</p> <p>Create a new file, <code>email-investors.js</code> within the <code>pages/api</code> directory.</p> <p>Add the code below to it:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">import</span> <span class="p">{</span> <span class="nx">Novu</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@novu/node</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">supabase</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/utils/supabase</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">novu</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Novu</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NEXT_PUBLIC_NOVU_API_KEY</span><span class="p">);</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">workflowTriggerID</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">new-opportunities</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">handler</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/** * Grab all investors */</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">supabase</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="dl">"</span><span class="s2">investors</span><span class="dl">"</span><span class="p">).</span><span class="nf">select</span><span class="p">(</span><span class="dl">"</span><span class="s2">*</span><span class="dl">"</span><span class="p">);</span> <span class="cm">/** * Map into a new array of subscriber data */</span> <span class="kd">const</span> <span class="nx">subscriberData</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">subscriberId</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nf">toString</span><span class="p">(),</span> <span class="na">email</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">email_reach</span><span class="p">,</span> <span class="p">};</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">extractIDs</span> <span class="o">=</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">data</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">item</span><span class="p">.</span><span class="nx">subscriberId</span><span class="p">);</span> <span class="p">};</span> <span class="kd">const</span> <span class="nx">subscriberIDs</span> <span class="o">=</span> <span class="nf">extractIDs</span><span class="p">(</span><span class="nx">subscriberData</span><span class="p">);</span> <span class="cm">/** * Bulk create subscribers on Novu */</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nx">subscribers</span><span class="p">.</span><span class="nf">bulkCreate</span><span class="p">(</span><span class="nx">subscriberData</span><span class="p">);</span> <span class="cm">/** * Create a Topic on Novu */</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nx">topics</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="na">key</span><span class="p">:</span> <span class="dl">"</span><span class="s2">all-investors</span><span class="dl">"</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">List of all investors on our platform</span><span class="dl">"</span><span class="p">,</span> <span class="p">});</span> <span class="cm">/** * Assign subscribers to Topic */</span> <span class="kd">const</span> <span class="nx">topicKey</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">all-investors</span><span class="dl">"</span><span class="p">;</span> <span class="cm">/** * Add all investors to the Topic */</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nx">topics</span><span class="p">.</span><span class="nf">addSubscribers</span><span class="p">(</span><span class="nx">topicKey</span><span class="p">,</span> <span class="p">{</span> <span class="na">subscribers</span><span class="p">:</span> <span class="nx">subscriberIDs</span><span class="p">,</span> <span class="p">});</span> <span class="cm">/** * Trigger workflow to the Topic */</span> <span class="k">await</span> <span class="nx">novu</span><span class="p">.</span><span class="nf">trigger</span><span class="p">(</span><span class="nx">workflowTriggerID</span><span class="p">,</span> <span class="p">{</span> <span class="na">to</span><span class="p">:</span> <span class="p">[{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Topic</span><span class="dl">"</span><span class="p">,</span> <span class="na">topicKey</span><span class="p">:</span> <span class="nx">topicKey</span> <span class="p">}],</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">({</span> <span class="na">finish</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span> <span class="p">}</span> </code></pre> </div> <p>There are a series of events happening in this file. I’ll explain the steps below:</p> <ol> <li>Fetch all investors data from Supabase</li> <li>Return an array of objects of the supabase data with “subscriberId” and “email”.</li> <li>Create all the investors as subscribers on Novu. Thankfully Novu has a `bulkCreate" function to create hundreds of subscribers at once instead of doing a complex loop yourself.</li> <li>Create a topic on Novu. Topics offers a great way of sending notifications to multiple subscribers at once. In this case, it’s perfect for us. Here, we created a “all-investors” topic and then proceeded to add all the investors as subscribers to the topic.</li> <li>The last step is what triggers the notification to all the investors. This is a simple syntax that triggers notifications to a topic. This means every subscriber belonging to that topic will receive a notification!</li> </ol> <p>This block of code is where I set the trigger ID of the new Email workflow i created.</p> <p><code></code><code>javascript<br> export const workflowTriggerID = "new-opportunities";<br> </code><code></code></p> <p>If you haven’t, go ahead and create an email workflow on Novu. Name the workflow and use the corresponding trigger ID like I did.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx13a7hjico24vg5lxfqb.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx13a7hjico24vg5lxfqb.png" alt="Email workflow"></a><br> Add content to the email workflow and save.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcoubrfoo7u2t1vas9k79.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcoubrfoo7u2t1vas9k79.png" alt="Email content"></a></p> <p><strong>Note:</strong> At this stage, it's crucial to ensure that an email provider has been selected from the <strong>Integrations Store.</strong> This allows Novu to identify the email provider to use for delivering the email.</p> <p>Run the API, <code>/api/email-investors</code>. The investors should get an email like this:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbjhfmk1pg22xszuv2brp.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbjhfmk1pg22xszuv2brp.png" alt="Novu Email"></a></p> <h2> Conclusion </h2> <p>The combination of two robust open-source tools, Novu and Supabase, enables the rapid development of apps with a rich notification experience.</p> <p>I hope you enjoyed building this simple investor list app as much as I did. Novu offers different channels such as Email, SMS, and Push, gives you a <a href="proxy.php?url=https://docs.novu.co/activity-feed/introduction#viewing-a-specific-subscribers-activity" rel="noopener noreferrer">detailed activity feed</a> to help debug your notifications and much more!</p> <p>Do you have an idea or an app you'd like to build with Novu and Supabase? We are excited to see the incredible apps you'll create. Don't hesitate to ask any questions or request support. You can find us on <a href="proxy.php?url=https://discord.gg/novu" rel="noopener noreferrer">Discord</a> and <a href="proxy.php?url=https://twitter.com/novuhq" rel="noopener noreferrer">Twitter</a>. Please feel free to reach out.</p> webdev notifications javascript supabase The Ultimate Guide to Laravel Reverb: Real-Time Notifications Prosper Otemuyiwa Tue, 09 Apr 2024 10:18:00 +0000 https://dev.to/novu/the-ultimate-guide-to-laravel-reverb-real-time-notifications-48h4 https://dev.to/novu/the-ultimate-guide-to-laravel-reverb-real-time-notifications-48h4 <p>You learned a lot about using Laravel Reverb in the <a href="proxy.php?url=https://novu.co/blog/the-ultimate-guide-to-laravel-reverb/" rel="noopener noreferrer">first part of this guide</a>. Now, you’ll learn how to add real-time notifications seamlessly to your Laravel apps.</p> <p>If you've used Laravel Nova, you're likely familiar with the Notification Center. But what if you didn't need to build one from scratch? Imagine being able to add a real-time notification center to your app in less than five minutes.</p> <p>If you're eager to explore the code immediately, you can <strong><a href="proxy.php?url=https://github.com/novuhq/laravel-reverb-app" rel="noopener noreferrer">view the completed code</a></strong> on GitHub. Let's dive in!</p> <h2> Introducing Novu </h2> <p>Novu is a notification infrastructure tool, built for engineering teams to help them build and set up rich product notification experiences.</p> <p>Novu provides embeddable Notification Center components, APIs, SDKs and more to help you manage product communication across multiple channels. It provides a full notification infrastructure that provides robust analytics, digest, notification center components, multi-channel notifications and hundreds of notification providers.</p> <h2> Set Up Novu </h2> <p>The first step is to sign up on <a href="proxy.php?url=https://web.novu.co" rel="noopener noreferrer">Novu.</a></p> <p>Now, run the following command to install the Novu Laravel SDK:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> composer require novu/novu-laravel </code></pre> </div> <p>Publish the configuration file using this command:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan vendor:publish <span class="nt">--tag</span><span class="o">=</span><span class="s2">"novu-laravel-config"</span> </code></pre> </div> <p>A configuration file named <code>novu.php</code> with some sensible defaults will be placed in your <code>config</code> directory. Open up your <code>.env</code> file and add the <code>NOVU_API_KEY</code> variable to it.</p> <p><strong>Note:</strong> Grab your <a href="proxy.php?url=https://web.novu.co/settings" rel="noopener noreferrer">API key from Settings</a> in your Novu dashboard.</p> <p>Before we can start sending and receiving notifications in our app, we need to set up a few things:</p> <ul> <li>Create a Novu workflow for sending notifications,</li> <li>Create a subscriber - recipient of notifications,</li> <li>Add a Novu Notification Center component inside our view to display real-time notifications.</li> </ul> <h2> <strong><a href="proxy.php?url=https://docs.novu.co/quickstarts/react#create-a-workflow" rel="noopener noreferrer"></a>Create a Novu Workflow</strong> </h2> <p>A workflow is a blueprint for notifications. It includes the following:</p> <ul> <li>Workflow name and Identifier</li> <li>Channels: - Email, SMS, Chat, In-App and Push.</li> <li>Channel Notification Content Editor</li> </ul> <p>Follow the steps below to create a workflow:</p> <ul> <li>Click <strong>Workflow</strong> on the left sidebar of your Novu dashboard.</li> <li>Click the <strong>Add a Workflow</strong> button on the top left. You can select a Blank workflow or use one of the existing templates.</li> <li>The name of the new workflow is currently <strong>“Untitled”</strong>. Rename it to <code>Laravel In-App Notifications</code> </li> <li>Select <strong>In-App</strong> as the channel you want to add.</li> </ul> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjyv5z96q7gx28r3cbcdq.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjyv5z96q7gx28r3cbcdq.png" alt="Select Channel"></a>5. Click on the recently added <strong>“In-App”</strong> channel and add the following text to it. Once you’re done, click <strong>“Update”</strong> to save your configuration.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpxh7y1khuipacwo264zl.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpxh7y1khuipacwo264zl.png" alt="In-App Content Editing"></a>The <code>{{deliveryStatus }},</code> and <code>{{deliveryHandler}}</code> are custom variables. This means that we can pass them to our <a href="proxy.php?url=https://docs.novu.co/quickstarts/general#pass-the-subscriber-information-in-the-trigger-quickest" rel="noopener noreferrer">payload</a> before we trigger a notification.</p> <p>You’ll see this when we add the code to trigger a notification.</p> <h2> <strong><a href="proxy.php?url=https://docs.novu.co/quickstarts/react#create-a-subscriber" rel="noopener noreferrer"></a>Create a subscriber</strong> </h2> <p>If you click <strong>“Subscriber”</strong> on the left sidebar of the <strong><a href="proxy.php?url=https://web.novu.co/subscribers" rel="noopener noreferrer">Novu dashboard</a></strong>, you’ll see the subscriber list. As a first time Novu user, it will be an empty list. Subscribers are your <strong>app users.</strong></p> <p>This implies that when a user registers an account in our app, we must also add them as a subscriber in Novu.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5n0v1ew297hixdl2cve2.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5n0v1ew297hixdl2cve2.png" alt="Subscriber creation"></a>For a test run, open your terminal and run the following script to create a subscriber:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> curl <span class="nt">--location</span> <span class="s1">'&lt;https://api.novu.co/v1/subscribers&gt;'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Accept: application/json'</span> <span class="se">\</span> <span class="nt">--header</span> <span class="s1">'Authorization: ApiKey &lt;NOVU_API_KEY&gt;'</span> <span class="se">\</span> <span class="nt">--data-raw</span> <span class="s1">'{ "firstName": "John", "lastName": "Doe", "email": "[email protected]", "phone": "+1234567890" }'</span> </code></pre> </div> <p><strong>Note:</strong> You can use the details of the user that is already signed up on the app. Refresh the Subscribers page on your Novu dashboard. You should see the recently added subscriber. The one you added via the terminal!</p> <p>The optimal approach is to add a subscriber through backend code. Just as the user is being added to the database, invoke the code to add the user as a subscriber on Novu. Let’s do that in our app.</p> <p>We utilized the Laravel JetStream kit. Therefore, our authentication and login logic should be located within the <strong>app/Actions/Fortify</strong> directory.</p> <p>Open up <code>app/Actions/Fortify/CreateNewUser.php</code> and modify the class to include Novu logic to create a subscriber on Novu from the user’s details.</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="cp">&lt;?php</span> <span class="kn">namespace</span> <span class="nn">App\Actions\Fortify</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">App\Models\User</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Hash</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Validator</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Laravel\Fortify\Contracts\CreatesNewUsers</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Laravel\Jetstream\Jetstream</span><span class="p">;</span> <span class="kd">class</span> <span class="nc">CreateNewUser</span> <span class="kd">implements</span> <span class="nc">CreatesNewUsers</span> <span class="p">{</span> <span class="kn">use</span> <span class="nc">PasswordValidationRules</span><span class="p">;</span> <span class="cd">/** * Validate and create a newly registered user. * * @param array&lt;string, string&gt; $input */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">create</span><span class="p">(</span><span class="kt">array</span> <span class="nv">$input</span><span class="p">):</span> <span class="kt">User</span> <span class="p">{</span> <span class="nc">Validator</span><span class="o">::</span><span class="nf">make</span><span class="p">(</span><span class="nv">$input</span><span class="p">,</span> <span class="p">[</span> <span class="s1">'name'</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="s1">'required'</span><span class="p">,</span> <span class="s1">'string'</span><span class="p">,</span> <span class="s1">'max:255'</span><span class="p">],</span> <span class="s1">'email'</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="s1">'required'</span><span class="p">,</span> <span class="s1">'string'</span><span class="p">,</span> <span class="s1">'email'</span><span class="p">,</span> <span class="s1">'max:255'</span><span class="p">,</span> <span class="s1">'unique:users'</span><span class="p">],</span> <span class="s1">'password'</span> <span class="o">=&gt;</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">passwordRules</span><span class="p">(),</span> <span class="s1">'terms'</span> <span class="o">=&gt;</span> <span class="nc">Jetstream</span><span class="o">::</span><span class="nf">hasTermsAndPrivacyPolicyFeature</span><span class="p">()</span> <span class="o">?</span> <span class="p">[</span><span class="s1">'accepted'</span><span class="p">,</span> <span class="s1">'required'</span><span class="p">]</span> <span class="o">:</span> <span class="s1">''</span><span class="p">,</span> <span class="p">])</span><span class="o">-&gt;</span><span class="nf">validate</span><span class="p">();</span> <span class="nv">$user</span> <span class="o">=</span> <span class="nc">User</span><span class="o">::</span><span class="nf">create</span><span class="p">([</span> <span class="s1">'name'</span> <span class="o">=&gt;</span> <span class="nv">$input</span><span class="p">[</span><span class="s1">'name'</span><span class="p">],</span> <span class="s1">'email'</span> <span class="o">=&gt;</span> <span class="nv">$input</span><span class="p">[</span><span class="s1">'email'</span><span class="p">],</span> <span class="s1">'password'</span> <span class="o">=&gt;</span> <span class="nc">Hash</span><span class="o">::</span><span class="nf">make</span><span class="p">(</span><span class="nv">$input</span><span class="p">[</span><span class="s1">'password'</span><span class="p">]),</span> <span class="p">]);</span> <span class="c1">// Create subscriber on Novu</span> <span class="nf">novu</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">createSubscriber</span><span class="p">([</span> <span class="s1">'subscriberId'</span> <span class="o">=&gt;</span> <span class="nv">$user</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">,</span> <span class="s1">'email'</span> <span class="o">=&gt;</span> <span class="nv">$user</span><span class="o">-&gt;</span><span class="n">email</span><span class="p">,</span> <span class="s1">'firstName'</span> <span class="o">=&gt;</span> <span class="nv">$user</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">,</span> <span class="p">])</span><span class="o">-&gt;</span><span class="nf">toArray</span><span class="p">();</span> <span class="k">return</span> <span class="nv">$user</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Reload your app, create a brand new user and check the <a href="proxy.php?url=https://web.novu.co/subscribers" rel="noopener noreferrer">Novu subscribers section</a> of your dashboard.</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9am6gt9zuk856b1cvcw5.png" alt="Subscribers page"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1cdf4irdeprcu48yi1y.png" alt="List of Subscribers"><strong>Set up &amp; Display Novu Notification Center in your Laravel App</strong> </h2> <p>Head over to <code>resources/views/components</code> directory.</p> <p>Create a <code>notification-center.blade.php</code> file in the directory and add the following code to it:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="o">&lt;</span><span class="nx">notification</span><span class="o">-</span><span class="nx">center</span><span class="o">-</span><span class="nx">component</span> <span class="nx">style</span><span class="o">=</span><span class="dl">"</span><span class="s2">{{ $style ?? '' }}</span><span class="dl">"</span> <span class="nx">application</span><span class="o">-</span><span class="nx">identifier</span><span class="o">=</span><span class="dl">"</span><span class="s2">{!! $appId ?? '' !!}</span><span class="dl">"</span> <span class="nx">subscriber</span><span class="o">-</span><span class="nx">id</span><span class="o">=</span><span class="dl">"</span><span class="s2">{!! $subscriberId ?? '' !!}</span><span class="dl">"</span> <span class="o">&gt;&lt;</span><span class="sr">/notification-center-component</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">script</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text/javascript</span><span class="dl">"</span><span class="o">&gt;</span> <span class="kd">let</span> <span class="nx">nc</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementsByTagName</span><span class="p">(</span><span class="dl">'</span><span class="s1">notification-center-component</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span> <span class="nx">nc</span><span class="p">.</span><span class="nx">onLoad</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">notification center loaded!</span><span class="dl">'</span><span class="p">);</span> <span class="o">&lt;</span><span class="sr">/script</span><span class="err">&gt; </span> </code></pre> </div> <p>Head over to <code>resources/views/layouts/app.blade.php</code> file. At the scripts section, add the following to invoke the Novu Notification Center component:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="o">&lt;</span><span class="nx">script</span> <span class="nx">src</span><span class="o">=</span><span class="dl">"</span><span class="s2">&lt;https://novu-web-component.netlify.app/index.js&gt;</span><span class="dl">"</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text/javascript</span><span class="dl">"</span> <span class="nx">defer</span><span class="o">&gt;&lt;</span><span class="sr">/script</span><span class="err">&gt; </span> </code></pre> </div> <p>In the body, call the notification center blade component like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="mf">...</span> <span class="o">&lt;</span><span class="n">div</span> <span class="n">class</span><span class="o">=</span><span class="s2">"min-h-screen bg-gray-100 dark:bg-gray-900"</span><span class="o">&gt;</span> <span class="o">@</span><span class="nf">livewire</span><span class="p">(</span><span class="s1">'navigation-menu'</span><span class="p">)</span> <span class="o">@</span><span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$header</span><span class="p">))</span> <span class="o">&lt;</span><span class="n">header</span> <span class="n">class</span><span class="o">=</span><span class="s2">"bg-white dark:bg-gray-800 shadow"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">div</span> <span class="n">class</span><span class="o">=</span><span class="s2">"max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"</span><span class="o">&gt;</span> <span class="p">{{</span> <span class="nv">$header</span> <span class="p">}}</span> <span class="o">&lt;/</span><span class="n">div</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">div</span> <span class="n">class</span><span class="o">=</span><span class="s2">"max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">x</span><span class="o">-</span><span class="n">notification</span><span class="o">-</span><span class="n">center</span> <span class="n">app</span><span class="o">-</span><span class="n">id</span><span class="o">=</span><span class="s2">"MavBpIkktq7-"</span> <span class="n">subscriber</span><span class="o">-</span><span class="n">id</span><span class="o">=</span><span class="s2">"{{ auth()-&gt;user()-&gt;id }}"</span> <span class="n">style</span><span class="o">=</span><span class="s2">"display: inline-flex;"</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">x</span><span class="o">-</span><span class="n">notification</span><span class="o">-</span><span class="n">center</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">div</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">header</span><span class="o">&gt;</span> <span class="o">@</span><span class="k">endif</span> <span class="o">&lt;</span><span class="n">livewire</span><span class="o">:</span><span class="n">delivery</span><span class="o">-</span><span class="n">history</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="n">main</span><span class="o">&gt;</span> <span class="p">{{</span> <span class="nv">$slot</span> <span class="p">}}</span> <span class="o">&lt;/</span><span class="n">main</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">div</span><span class="o">&gt;</span> <span class="mf">...</span> </code></pre> </div> <p>From the code above, you can see that we added the following:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"</span><span class="nt">&gt;</span> <span class="nt">&lt;x-notification-center</span> <span class="na">app-id=</span><span class="s">"MavBpIkktq7-"</span> <span class="na">subscriber-id=</span><span class="s">"{{ auth()-&gt;user()-&gt;id }}"</span> <span class="na">style=</span><span class="s">"display: inline-flex;"</span><span class="nt">&gt;</span> <span class="nt">&lt;/x-notification-center&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre> </div> <p><strong>Note:</strong> I have added the <strong>APP ID</strong> &amp; <strong>Subscriber ID</strong>. The value of the <strong>APP ID</strong> is from the Novu Settings dashboard while the <strong>Subscriber ID</strong> is the ID of the logged in user.</p> <p>Now you should have something like this on your dashboard showing the notification bell:</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgxwaqo6sot26c49c7yum.png" alt="Dashboard showing the notification bell"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftimuw6gkyafl16z48vfu.png" alt="Notification bell">Trigger Real-time Notifications in your Laravel App </h2> <p>Open up <code>app/Livewire/DeliveryHistory.php</code> file. Here, we will add Novu code to trigger notification when a new delivery status is entered.</p> <p>Add the code to the <code>submitStatus()</code> function like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="k">public</span> <span class="k">function</span> <span class="n">submitStatus</span><span class="p">()</span> <span class="p">{</span> <span class="nc">PackageSent</span><span class="o">::</span><span class="nf">dispatch</span><span class="p">(</span><span class="nf">auth</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">user</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">status</span><span class="p">,</span> <span class="nc">Carbon</span><span class="o">::</span><span class="nf">now</span><span class="p">());</span> <span class="cd">/** * Trigger Novu to fire the in-app notifications */</span> <span class="nf">novu</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">triggerEvent</span><span class="p">([</span> <span class="s1">'name'</span> <span class="o">=&gt;</span> <span class="s1">'laravel-in-app-notifications'</span><span class="p">,</span> <span class="s1">'payload'</span> <span class="o">=&gt;</span> <span class="p">[</span> <span class="s1">'deliveryStatus'</span> <span class="o">=&gt;</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">status</span><span class="p">,</span> <span class="s1">'deliveryHandler'</span> <span class="o">=&gt;</span> <span class="nf">auth</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">user</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">name</span> <span class="p">],</span> <span class="s1">'to'</span> <span class="o">=&gt;</span> <span class="p">[</span> <span class="s1">'subscriberId'</span> <span class="o">=&gt;</span> <span class="nf">auth</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">user</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">,</span> <span class="p">]</span> <span class="p">])</span><span class="o">-&gt;</span><span class="nf">toArray</span><span class="p">();</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nb">reset</span><span class="p">(</span><span class="s1">'status'</span><span class="p">);</span> <span class="p">}</span> </code></pre> </div> <p>The value of the <code>name</code> is the workflow trigger ID. Open up the Novu workflow we created on Novu dashboard, you will be able to identify the trigger ID like so:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8d7yfr2w2notnv817hm.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8d7yfr2w2notnv817hm.png" alt="Workflow Trigger ID"></a>Next, we included both <code>deliveryStatus</code> and <code>deliveryHandler</code> as payload items in the trigger code call. This allows our workflow to receive and display them as part of the notification content in the Notification Center.</p> <p>Finally, we add the ID of the subscriber that we want to see the real-time notification when it has been triggered. This should always be the ID of the logged-in-user so that they can see the notification once it comes into the app.</p> <p><strong>One more thing…</strong></p> <p>Open up <code>routes/channel.php</code> and modify it to the code below:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="cp">&lt;?php</span> <span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Broadcast</span><span class="p">;</span> <span class="nc">Broadcast</span><span class="o">::</span><span class="nf">channel</span><span class="p">(</span><span class="s1">'delivery'</span><span class="p">,</span> <span class="k">function</span> <span class="p">(</span><span class="nv">$user</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span> <span class="p">});</span> </code></pre> </div> <p>We need to do this (<em>return true always regardless of whoever is logged in</em>) else we won’t be allowed to enter a status because the user that is logged in is no longer of ID 1.</p> <p>Reload your app and attempt to add a status. You'll notice the notification appear in real time in the Notification Center. It's quick, instantaneous, and visually pleasing!</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0c9nymerfquygbiqpuo.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0c9nymerfquygbiqpuo.png"></a>You can try different things in the Notification center pop up:</p> <ul> <li>You can mark an individual message as read.</li> <li>You can delete one individual message</li> <li>You can mark all as read at once.</li> </ul> <p>Novu makes it a breeze to add as many notification channels as possible without having to incorporate the logic of each notification provider. Write once, run as many channels as possible!</p> <p>Check out the <a href="proxy.php?url=https://docs.novu.co/notification-center/introduction" rel="noopener noreferrer">documentation on all the many things</a> you can do with Novu’s Notification Center</p> <h2> Conclusion </h2> <p>We've covered how to use Laravel Reverb, from setup and configuration to building a real-time app. You've also learned how to leverage Novu in setting up effective, scalable real-time in-app notifications.</p> <p>Laravel and Novu are powerful tools. When combined, they provide everything you need to build fast, robust, scalable, and real-time apps. I'm excited to see your next app.</p> <p>If you have any questions, feel free to explore our documentation and quickstarts. You can also find me on <a href="proxy.php?url=https://bit.ly/3vULbrL" rel="noopener noreferrer">Discord</a> and <a href="proxy.php?url=https://bit.ly/44axm4W" rel="noopener noreferrer">Twitter</a>. Don't hesitate to reach out.</p> webdev php laravel notifications The Ultimate Guide to Laravel Reverb Prosper Otemuyiwa Tue, 09 Apr 2024 10:11:13 +0000 https://dev.to/novu/the-ultimate-guide-to-laravel-reverb-275o https://dev.to/novu/the-ultimate-guide-to-laravel-reverb-275o <p>Laravel Reverb is a first-party WebSocket server for Laravel applications, providing real-time communication capabilities between the client and server seamlessly.</p> <p>Laravel Reverb has numerous appealing features, including:</p> <ul> <li>Speed and scalability.</li> <li>Support for thousands of simultaneous connections.</li> <li>Integration with existing Laravel broadcasting features.</li> <li>Compatibility with Laravel Echo.</li> <li>First-class integration and deployment with Laravel Forge.</li> </ul> <p>In this guide, I'll demonstrate how to use Laravel Reverb to develop a real-time Laravel app. You'll learn about channels, events, broadcasting, and how to use Laravel Reverb to create quick and real-time applications in Laravel.</p> <p>If you're eager to explore the code immediately, you can <strong><a href="proxy.php?url=https://github.com/novuhq/laravel-reverb-app" rel="noopener noreferrer">view the completed code</a></strong> on GitHub. Let's get started!</p> <h2> Install a Fresh Laravel App </h2> <p>Go ahead and create a new laravel app.</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> laravel new carryon </code></pre> </div> <p>I love to start new Laravel apps with one of the starter kits that ships with login, registration, email verification, etc. In this guide, I’ll use <a href="proxy.php?url=https://bit.ly/3UvPjrF" rel="noopener noreferrer">Laravel JetStream with Livewire</a>.</p> <p>You can follow the Laravel console prompt to ensure everything is set up properly with the right starter kit.</p> <p>Run your migrations to set up the database with the users, jobs, cache &amp; access tokens table.</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan migrate </code></pre> </div> <p>Now, run your app with <code>php artisan serve</code>. For folks with Herd or Valet, your app should already be available on <code>http://carryon.test</code></p> <p>You should have something like this:- A fresh new Laravel app with JetStream enabled. So beautiful! 🎉</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc80rpk6xlnf0g44ze804.png" alt="New Installation">Install Laravel Reverb </h2> <p>Now, we need to install Laravel Reverb - our WebSocket Server into the Laravel app.</p> <p>Run the following command in your console and choose the <strong>Yes</strong> option for any of the prompts that show up:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan <span class="nb">install</span>:broadcasting </code></pre> </div> <p>This command will do the following:</p> <ul> <li>Publish the <strong>broadcasting</strong> config and <strong>channels</strong> route file.</li> <li>Install Laravel Reverb (WebSocket server)</li> <li>Install and build the Node dependencies required.</li> </ul> <p>Next, open two new terminals to start up the reverb server and also run the client side.</p> <p><strong>First terminal:</strong> Start and run reverb server</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan reverb:start </code></pre> </div> <p>The reverb server is usually run on the 8080 port by default. You can see that in your console. If you need to specify a custom host or port, you may do so via the <strong><code>--host</code></strong> and <strong><code>--port</code></strong> options when starting the server like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan reverb:start <span class="nt">--host</span><span class="o">=</span>127.0.0.1 <span class="nt">--port</span><span class="o">=</span>9000 </code></pre> </div> <p><strong>Second terminal:</strong> Run Vite to ensure any changes on the client is hot reloaded &amp; instant.</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> npm run dev </code></pre> </div> <p>Check your <code>.env</code> file. You will notice a few additions to it. It added the credentials for running Reverb. And for the frontend to connect with the Reverb server.</p> <p>The <strong>BROADCAST_CONNECTION</strong> has been set to use <strong>reverb</strong>. Alternative broadcast drivers are <code>pusher</code>, <code>ably</code> and <code>log</code>.</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="nx">BROADCAST_CONNECTION</span><span class="o">=</span><span class="nx">reverb</span> <span class="p">...</span> <span class="nx">REVERB_APP_ID</span><span class="o">=</span><span class="mi">872050</span> <span class="nx">REVERB_APP_KEY</span><span class="o">=</span><span class="nx">ed5zsi5ebpdmawcqbwva</span> <span class="nx">REVERB_APP_SECRET</span><span class="o">=</span><span class="nx">zbttdgtacvuhfdo3dl0o</span> <span class="nx">REVERB_HOST</span><span class="o">=</span><span class="dl">"</span><span class="s2">localhost</span><span class="dl">"</span> <span class="nx">REVERB_PORT</span><span class="o">=</span><span class="mi">8080</span> <span class="nx">REVERB_SCHEME</span><span class="o">=</span><span class="nx">http</span> <span class="nx">VITE_REVERB_APP_KEY</span><span class="o">=</span><span class="dl">"</span><span class="s2">${REVERB_APP_KEY}</span><span class="dl">"</span> <span class="nx">VITE_REVERB_HOST</span><span class="o">=</span><span class="dl">"</span><span class="s2">${REVERB_HOST}</span><span class="dl">"</span> <span class="nx">VITE_REVERB_PORT</span><span class="o">=</span><span class="dl">"</span><span class="s2">${REVERB_PORT}</span><span class="dl">"</span> <span class="nx">VITE_REVERB_SCHEME</span><span class="o">=</span><span class="dl">"</span><span class="s2">${REVERB_SCHEME}</span><span class="dl">"</span> </code></pre> </div> <p>One more thing. Open up <code>resources/js/echo.js</code> file:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="k">import</span> <span class="nx">Echo</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">laravel-echo</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">Pusher</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">pusher-js</span><span class="dl">'</span><span class="p">;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">Pusher</span> <span class="o">=</span> <span class="nx">Pusher</span><span class="p">;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">Echo</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Echo</span><span class="p">({</span> <span class="na">broadcaster</span><span class="p">:</span> <span class="dl">'</span><span class="s1">reverb</span><span class="dl">'</span><span class="p">,</span> <span class="na">key</span><span class="p">:</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_REVERB_APP_KEY</span><span class="p">,</span> <span class="na">wsHost</span><span class="p">:</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_REVERB_HOST</span><span class="p">,</span> <span class="na">wsPort</span><span class="p">:</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_REVERB_PORT</span> <span class="o">??</span> <span class="mi">80</span><span class="p">,</span> <span class="na">wssPort</span><span class="p">:</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_REVERB_PORT</span> <span class="o">??</span> <span class="mi">443</span><span class="p">,</span> <span class="na">forceTLS</span><span class="p">:</span> <span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_REVERB_SCHEME</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">https</span><span class="dl">'</span><span class="p">)</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">https</span><span class="dl">'</span><span class="p">,</span> <span class="na">enabledTransports</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">ws</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">wss</span><span class="dl">'</span><span class="p">],</span> <span class="p">});</span> </code></pre> </div> <p>This file shows how Laravel Echo connects with the Reverb server. If you take a step further into the <code>bootstrap.js</code> file, you’ll see the echo file was imported. This means on start up, our app now uses and connects to reverb!</p> <h2> Show Workings - Test Reverb Connection </h2> <p>We have set up Laravel and Laravel Reverb. How do we test that our WebSocket server is working and the app is properly set up receive events on the Laravel frontend?</p> <p><strong>Step 1:</strong> Head over to the <code>resources/js/echo.js</code> file again. Here, we will set up a channel and tell it to listen to an event (<em>doesn’t matter that we haven’t created it yet</em>).</p> <p>Add the following code to the file:</p> <div class="highlight js-code-highlight"> <pre class="highlight javascript"><code> <span class="cm">/** * Testing Channels &amp; Events &amp; Connections */</span> <span class="nb">window</span><span class="p">.</span><span class="nx">Echo</span><span class="p">.</span><span class="nf">channel</span><span class="p">(</span><span class="dl">"</span><span class="s2">delivery</span><span class="dl">"</span><span class="p">).</span><span class="nf">listen</span><span class="p">(</span><span class="dl">"</span><span class="s2">PackageSent</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span> <span class="p">});</span> </code></pre> </div> <p>Here, we have created a <strong>delivery</strong> channel &amp; are listening on a <strong>PackageSent</strong> <em>(This is imaginary for now)</em> ****event.</p> <p><strong>Step 2:</strong> Restart the reverb server but with this command:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan reverb:start <span class="nt">--debug</span> </code></pre> </div> <p>We added a debug option to allow us to see the logs of the WebSocket connections from the terminal. It’s also a good idea to debug your realtime connections problem if it’s not working as intended.</p> <p><strong>Step 3:</strong> Now, reload your Laravel app. Click on the Login or Dashboard link and open the chrome dev tools. Ensure you narrow it down to WS as shown below.</p> <p>You should see the realtime connections and events in the devtools like so:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykrs3z6vtl3n194z590t.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykrs3z6vtl3n194z590t.png" alt="Real-time events"></a><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vcmzfa0wt6466ex6t4n.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vcmzfa0wt6466ex6t4n.png" alt="Real-time log"></a>See the delivery channel we created. Now, you can also see the ping and pong events. This means our server is ready and waiting to stream real-time connections. Yaaay!</p> <p>You can also see the evidence on the server. Check the console of the reverb debug. You should see something like this:</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ee1ou2kyoy3ig9w2l52.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ee1ou2kyoy3ig9w2l52.png" alt="Server Reverb logs"></a><strong>Step 4:</strong> Create the PackageSent event so that we can send events.</p> <p>Run the following artisan command to create it quickly:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan make:event PackageSent </code></pre> </div> <p>Open up the <code>app/Events/PackageSent.php</code> to see the event boilerplate created.</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="cp">&lt;?php</span> <span class="kn">namespace</span> <span class="nn">App\Events</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\Channel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\InteractsWithSockets</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\PresenceChannel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\PrivateChannel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Contracts\Broadcasting\ShouldBroadcast</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Foundation\Events\Dispatchable</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Queue\SerializesModels</span><span class="p">;</span> <span class="kd">class</span> <span class="nc">PackageSent</span> <span class="p">{</span> <span class="kn">use</span> <span class="nc">Dispatchable</span><span class="p">,</span> <span class="nc">InteractsWithSockets</span><span class="p">,</span> <span class="nc">SerializesModels</span><span class="p">;</span> <span class="cd">/** * Create a new event instance. */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">()</span> <span class="p">{</span> <span class="c1">//</span> <span class="p">}</span> <span class="cd">/** * Get the channels the event should broadcast on. * * @return array&lt;int, \Illuminate\Broadcasting\Channel&gt; */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">broadcastOn</span><span class="p">():</span> <span class="kt">array</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[</span> <span class="k">new</span> <span class="nc">PrivateChannel</span><span class="p">(</span><span class="s1">'channel-name'</span><span class="p">),</span> <span class="p">];</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Now, replace it with the code below:</p> <p><code>app/Events/PackageSent.php</code></p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="cp">&lt;?php</span> <span class="kn">namespace</span> <span class="nn">App\Events</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\Channel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\InteractsWithSockets</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\PresenceChannel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\PrivateChannel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Contracts\Broadcasting\ShouldBroadcast</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Foundation\Events\Dispatchable</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Queue\SerializesModels</span><span class="p">;</span> <span class="kd">class</span> <span class="nc">PackageSent</span> <span class="kd">implements</span> <span class="nc">ShouldBroadCast</span> <span class="p">{</span> <span class="kn">use</span> <span class="nc">Dispatchable</span><span class="p">,</span> <span class="nc">InteractsWithSockets</span><span class="p">,</span> <span class="nc">SerializesModels</span><span class="p">;</span> <span class="cd">/** * Create a new event instance. */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">(</span> <span class="k">public</span> <span class="kt">string</span> <span class="nv">$status</span><span class="p">,</span> <span class="k">public</span> <span class="kt">string</span> <span class="nv">$deliveryHandler</span> <span class="p">)</span> <span class="p">{</span> <span class="p">}</span> <span class="cd">/** * Get the channels the event should broadcast on. * * @return Illuminate\Broadcasting\Channel */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">broadcastOn</span><span class="p">():</span> <span class="kt">Channel</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">Channel</span><span class="p">(</span><span class="s1">'delivery'</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>The following happened:</p> <ul> <li>Now the class implements <code>ShouldBroadCast</code>. This means the event should be broadcasted via Laravel Echo.</li> <li>Passed in two parameters to the constructor. Status of the package and the handler. We need this to know the status of the package and who is responsible for it at anytime.</li> <li>The <code>broadCastOn()</code> method by default allows us to broadcast to many channels at a time. However, in this case we want to broadcast to only one. So it was modified to return only one channel; <code>delivery</code> , instead of an array of channels.</li> </ul> <p><strong>Note:</strong> This is a public channel. We are broadcasting the PackageSent event on a public channel. The channels are either instances of <strong><code>Channel</code></strong>, <strong><code>PrivateChannel</code></strong>, or <strong><code>PresenceChannel</code></strong>. PrivateChannel and PresenceChannel require authorization for any user to subscribe to while any random user can subscribe public channels.</p> <p><strong>Step 5:</strong> Dispatch the PackageSent event.</p> <p>Open your terminal and fire up the amazing artisan tinker by running the following command:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan tinker </code></pre> </div> <p>Just before we write code in the tinker terminal. Open a new terminal and ensure your queue is running like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan queue:listen </code></pre> </div> <p><strong>Note:</strong> This is very important because event jobs are queued to the database by default. So our queue needs to be able to listen to the jobs to fire them.</p> <p>Now call the event and dispatch it like so within the tinker terminal:</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> <span class="o">&gt;</span> use App<span class="se">\E</span>vents<span class="se">\P</span>ackageSent<span class="p">;</span> <span class="o">&gt;</span> PackageSent::dispatch<span class="o">(</span><span class="s1">'processed'</span>, <span class="s1">'prosper'</span><span class="o">)</span><span class="p">;</span> <span class="o">&gt;</span> PackageSent::dispatch<span class="o">(</span><span class="s1">'delivered'</span>, <span class="s1">'olamide'</span><span class="o">)</span><span class="p">;</span> </code></pre> </div> <p>This will go ahead and fire the <code>PackageSent</code> event twice.</p> <p>Check your queue console to see if the jobs were processed.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m212ucb1di3jgtzquga.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m212ucb1di3jgtzquga.png" alt="Job processing"></a>Yaaay, it was processed!</p> <p>Now, go ahead and check your Laravel app in the dev tools console. You should see the dispatched event and the data we sent.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftossfrmj5xtlsmk3fb82.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftossfrmj5xtlsmk3fb82.png" alt="Events"></a><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsnzdwhkhiaf320f65oo.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsnzdwhkhiaf320f65oo.png" alt="Stream of events"></a>Woow! Just amazing!!</p> <p>We’ve been doing this from the terminal. Next, let’s do this straight from the UI with user interaction.</p> <h2> Build A Real-time Delivery History UI </h2> <p>Open your terminal and run the command below to create a livewire component.</p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code> php artisan make:livewire DeliveryHistory </code></pre> </div> <p>Laravel will create a class and corresponding delivery-history blade view for the UI elements.</p> <p>Open up <code>resources/views/layouts/app.blade.php</code> file:</p> <p>Add the delivery-history livewire component just below the <code>@if</code> code block in the code like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="mf">....</span> <span class="o">&lt;</span><span class="n">body</span> <span class="n">class</span><span class="o">=</span><span class="s2">"font-sans antialiased"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">x</span><span class="o">-</span><span class="n">banner</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="n">div</span> <span class="n">class</span><span class="o">=</span><span class="s2">"min-h-screen bg-gray-100 dark:bg-gray-900"</span><span class="o">&gt;</span> <span class="o">@</span><span class="nf">livewire</span><span class="p">(</span><span class="s1">'navigation-menu'</span><span class="p">)</span> <span class="o">@</span><span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$header</span><span class="p">))</span> <span class="o">&lt;</span><span class="n">header</span> <span class="n">class</span><span class="o">=</span><span class="s2">"bg-white dark:bg-gray-800 shadow"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="n">div</span> <span class="n">class</span><span class="o">=</span><span class="s2">"max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"</span><span class="o">&gt;</span> <span class="p">{{</span> <span class="nv">$header</span> <span class="p">}}</span> <span class="o">&lt;/</span><span class="n">div</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">header</span><span class="o">&gt;</span> <span class="o">@</span><span class="k">endif</span> <span class="o">&lt;</span><span class="n">livewire</span><span class="o">:</span><span class="n">delivery</span><span class="o">-</span><span class="n">history</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="n">main</span><span class="o">&gt;</span> <span class="p">{{</span> <span class="nv">$slot</span> <span class="p">}}</span> <span class="o">&lt;/</span><span class="n">main</span><span class="o">&gt;</span> <span class="o">&lt;/</span><span class="n">div</span><span class="o">&gt;</span> <span class="o">@</span><span class="nf">stack</span><span class="p">(</span><span class="s1">'modals'</span><span class="p">)</span> <span class="o">@</span><span class="n">livewireScripts</span> <span class="o">&lt;/</span><span class="n">body</span><span class="o">&gt;</span> </code></pre> </div> <p>Open up <code>resources/views/livewire/delivery-history.blade.php</code> file &amp; let’s add a full blown UI to it. Copy the code below and add it:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"py-12"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"space-y-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"rounded-lg max-w-7xl mx-auto sm:px-6 lg:px-8 dark:bg-gray-800"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"py-2"</span><span class="nt">&gt;</span> <span class="nt">&lt;form</span> <span class="na">wire:submit.prevent=</span><span class="s">"submitStatus"</span> <span class="na">class=</span><span class="s">"flex gap-2"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">placeholder=</span><span class="s">"Enter delivery status....."</span> <span class="na">wire:model=</span><span class="s">"status"</span> <span class="na">x-ref=</span><span class="s">"statusInput"</span> <span class="na">name=</span><span class="s">"status"</span> <span class="na">id=</span><span class="s">"status"</span> <span class="na">class=</span><span class="s">"block w-full"</span> <span class="nt">/&gt;</span> <span class="nt">&lt;button</span> <span class="na">class=</span><span class="s">" hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded"</span><span class="nt">&gt;</span> ENTER <span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;/form&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"mt-5"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"rounded-lg max-w-7xl mx-auto sm:px-6 lg:px-8 dark:bg-gray-800"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex items-center justify-between"</span><span class="nt">&gt;&lt;h5</span> <span class="na">class=</span><span class="s">"forge-h5"</span><span class="nt">&gt;</span>Package Delivery History<span class="nt">&lt;/h5&gt;&lt;/div&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"py-2"</span><span class="nt">&gt;</span> <span class="nt">&lt;table</span> <span class="na">class=</span><span class="s">"w-full text-left"</span><span class="nt">&gt;</span> @if( count($packageStatuses) &gt; 0) <span class="nt">&lt;thead</span> <span class="na">class=</span><span class="s">"text-gray-500"</span><span class="nt">&gt;</span> <span class="nt">&lt;tr</span> <span class="na">class=</span><span class="s">"h-10"</span><span class="nt">&gt;</span> <span class="nt">&lt;th</span> <span class="na">class=</span><span class="s">"pr-4 font-normal"</span><span class="nt">&gt;</span>User<span class="nt">&lt;/th&gt;</span> <span class="nt">&lt;th</span> <span class="na">class=</span><span class="s">"w-full pr-4 font-normal"</span><span class="nt">&gt;</span>Written Status<span class="nt">&lt;/th&gt;</span> <span class="nt">&lt;th</span> <span class="na">class=</span><span class="s">"pr-4 font-normal"</span><span class="nt">&gt;</span>Time<span class="nt">&lt;/th&gt;</span> <span class="nt">&lt;th</span> <span class="na">class=</span><span class="s">"pr-4 font-normal"</span><span class="nt">&gt;</span>Status<span class="nt">&lt;/th&gt;</span> <span class="nt">&lt;th&gt;&lt;/th&gt;</span> <span class="nt">&lt;/tr&gt;</span> <span class="nt">&lt;/thead&gt;</span> <span class="nt">&lt;tbody</span> <span class="na">class=</span><span class="s">"max-w-full text-white"</span><span class="nt">&gt;</span> @foreach($packageStatuses as $status) <span class="nt">&lt;tr</span> <span class="na">class=</span><span class="s">"h-12 border-t border-gray-100 dark:border-gray-700"</span><span class="nt">&gt;</span> <span class="nt">&lt;td</span> <span class="na">class=</span><span class="s">"whitespace-nowrap pr-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex items-center"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-truncate w-32"</span><span class="nt">&gt;</span> {{ $status['deliveryPersonnel'] }}<span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/td&gt;</span> <span class="nt">&lt;td</span> <span class="na">class=</span><span class="s">"whitespace-nowrap pr-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex items-center"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-truncate w-32"</span><span class="nt">&gt;</span>{{ $status['deliveryStatus'] }}<span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/td&gt;</span> <span class="nt">&lt;td</span> <span class="na">class=</span><span class="s">"whitespace-nowrap pr-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex items-center"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-truncate w-32"</span><span class="nt">&gt;</span>{{ Carbon\\\\Carbon::parse($status['deliveryTime'])-&gt;diffForHumans() }} <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/td&gt;</span> <span class="nt">&lt;td</span> <span class="na">class=</span><span class="s">"whitespace-nowrap pr-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex items-center"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-truncate w-32"</span><span class="nt">&gt;</span> @if ($status['deliveryStatus'] == 'Port') <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"h-2 rounded-full bg-blue-600 transition-all transition-2s ease-in-out"</span><span class="nt">&gt;</span> <span class="nt">&lt;/div&gt;</span> @endif @if ($status['deliveryStatus'] == 'Processing') <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"h-2 rounded-full bg-yellow-600 transition-all transition-2s ease-in-out"</span><span class="nt">&gt;</span> <span class="nt">&lt;/div&gt;</span> @endif @if ($status['deliveryStatus'] == 'Shipped') <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"h-2 rounded-full bg-pink-600 transition-all transition-2s ease-in-out"</span><span class="nt">&gt;</span> <span class="nt">&lt;/div&gt;</span> @endif @if ($status['deliveryStatus'] == 'Delivered') <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"h-2 rounded-full bg-green-600 transition-all transition-2s ease-in-out"</span><span class="nt">&gt;</span> <span class="nt">&lt;/div&gt;</span> @endif @if (!in_array($status['deliveryStatus'], ['Port', 'Processing', 'Shipped', 'Delivered'])) <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"h-2 rounded-full bg-red-600 transition-all transition-2s ease-in-out"</span><span class="nt">&gt;</span> <span class="nt">&lt;/div&gt;</span> @endif <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/td&gt;</span> <span class="nt">&lt;/tr&gt;</span> @endforeach <span class="nt">&lt;/tbody&gt;</span> @else <span class="nt">&lt;h3&gt;</span> No History yet... <span class="nt">&lt;/h3&gt;</span> @endif <span class="nt">&lt;/table&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre> </div> <p>Let’s look at the code above for a bit and understand what’s going on:</p> <ul> <li>There’s a livewire form field with an input text field and button.</li> <li>When the button is clicked, it calls the <code>submitStatus</code> function. Now this function will be defined later in the <code>DeliveryHistory</code> class component.</li> <li>In the table section, we are looping over a <code>$packageStatuses</code> array variable and displaying the contents in the UI.</li> <li>If the <code>$packageStatuses</code> array is empty, we show a “No History yet…” section.</li> </ul> <h2> Wire Up The Delivery History Class Component </h2> <p>Open up the <code>app/Livewire/DeliveryHistory.php</code> class and replace the content with the code below:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="cp">&lt;?php</span> <span class="kn">namespace</span> <span class="nn">App\Livewire</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Carbon\Carbon</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Livewire\Component</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Livewire\Attributes\On</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">App\Events\PackageSent</span><span class="p">;</span> <span class="kd">class</span> <span class="nc">DeliveryHistory</span> <span class="kd">extends</span> <span class="nc">Component</span> <span class="p">{</span> <span class="k">public</span> <span class="kt">array</span> <span class="nv">$packageStatuses</span> <span class="o">=</span> <span class="p">[</span> <span class="p">];</span> <span class="k">public</span> <span class="kt">string</span> <span class="nv">$status</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span> <span class="k">public</span> <span class="k">function</span> <span class="n">submitStatus</span><span class="p">()</span> <span class="p">{</span> <span class="nc">PackageSent</span><span class="o">::</span><span class="nf">dispatch</span><span class="p">(</span><span class="nf">auth</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">user</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">status</span><span class="p">,</span> <span class="nc">Carbon</span><span class="o">::</span><span class="nf">now</span><span class="p">());</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nb">reset</span><span class="p">(</span><span class="s1">'status'</span><span class="p">);</span> <span class="p">}</span> <span class="na">#[On('echo:delivery,PackageSent')]</span> <span class="k">public</span> <span class="k">function</span> <span class="n">onPackageSent</span><span class="p">(</span><span class="nv">$event</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">packageStatuses</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$event</span><span class="p">;</span> <span class="p">}</span> <span class="k">public</span> <span class="k">function</span> <span class="n">render</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">view</span><span class="p">(</span><span class="s1">'livewire.delivery-history'</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Let’s break down what’s happening the code above and see how it connects with the blade UI.</p> <ul> <li>The <code>render()</code> method fetches and display the content of the delivery-history blade file to the UI.</li> <li>There are two class variables, <code>$status</code> and <code>$packageStatuses</code>. Livewire automatically makes them accessible from the corresponding blade view.</li> <li>The <code>submitStatus</code>() method is called when the form is submitted from the UI via livewire. In this method, we dispatch the <code>PackageSent</code> event with 3 arguments. The logged-in user’s name, the value of the text field in the UI, and the current time. When the <code>PackageSent</code> event is dispatched, how do we get the result of the event real-time via Reverb?</li> <li>Laravel livewire has a seamless way of retrieving real-time events with <a href="proxy.php?url=https://livewire.laravel.com/docs/events#listening-for-echo-events" rel="noopener noreferrer">Laravel Echo via the On Attribute</a>. We defined a function <code>onPackageSent()</code> that dumps the payload of the recently dispatched $event into the <code>$packageStatuses</code> array. The attribute <code>#[On('echo:delivery,PackageSent')]</code> makes it possible for us to specify the channel name and the event for livewire to listen to! Feels magical!</li> </ul> <h2> Modify the PackageSent Laravel Event </h2> <p>Before we test the app, we need to modify the constructor arguments of the <code>PackageSent</code> event class.</p> <p>Open up <code>app/Events/PackageSent.php</code> and modify the constructor to take in 3 arguments; <code>$deliveryPersonnel, $deliveryStatus, $deliveryTime</code>.</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="cp">&lt;?php</span> <span class="kn">namespace</span> <span class="nn">App\Events</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\Channel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\InteractsWithSockets</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\PresenceChannel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Broadcasting\PrivateChannel</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Contracts\Broadcasting\ShouldBroadcast</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Foundation\Events\Dispatchable</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Illuminate\Queue\SerializesModels</span><span class="p">;</span> <span class="kd">class</span> <span class="nc">PackageSent</span> <span class="kd">implements</span> <span class="nc">ShouldBroadCast</span> <span class="p">{</span> <span class="kn">use</span> <span class="nc">Dispatchable</span><span class="p">,</span> <span class="nc">InteractsWithSockets</span><span class="p">,</span> <span class="nc">SerializesModels</span><span class="p">;</span> <span class="cd">/** * Create a new event instance. */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">(</span> <span class="k">public</span> <span class="kt">string</span> <span class="nv">$deliveryPersonnel</span><span class="p">,</span> <span class="k">public</span> <span class="kt">string</span> <span class="nv">$deliveryStatus</span><span class="p">,</span> <span class="k">public</span> <span class="kt">string</span> <span class="nv">$deliveryTime</span> <span class="p">)</span> <span class="p">{</span> <span class="p">}</span> <span class="cd">/** * Get the channels the event should broadcast on. * * @return Illuminate\Broadcasting\Channel */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">broadcastOn</span><span class="p">():</span> <span class="kt">Channel</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">Channel</span><span class="p">(</span><span class="s1">'delivery'</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <h2> Reload and Test The App </h2> <p>Now, you can test the app.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9r6cqw9scmmyilkec19.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9r6cqw9scmmyilkec19.png" alt="Test the app"></a>Enter any status in the textfield and hit the Enter button OR hit Enter on your keyboard and watch everything come together.</p> <p><strong>Note:</strong> In the delivery history blade view, we defined a list of statuses: <code>Port</code>, <code>Processing</code>, <code>Shipped</code>, <code>Delivered</code>.</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55ebeiuhl40sc2tm1zau.png" alt="Statuses"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj2gkvelwi09qeqdkznmb.png" alt="List of statuses">Test The App While Firing Events From The Console </h2> <p>We have been able to test the app via the UI and it works really well!</p> <p>Now, open up tinker again and fire the event and watch how the UI updates in realtime! 🎉</p> <h2> <img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkggx5yl9t5w6rmmpefoo.png" alt="Tinker processing">BroadCast Events Only To A Private Channel </h2> <p>We’ve been listening and broadcasting events on a public channel. Now, let’s see how to do this securely on a private channel.</p> <p>We want to restrict subscription to our delivery channel to authorized users only. Let's make some changes in several areas across the app.</p> <p><strong>Step 1:</strong> Change Channel to <code>PrivateChannel</code> in <code>PackageSent</code> Event.</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="mf">...</span> <span class="cd">/** * Get the channels the event should broadcast on. * * @return Illuminate\Broadcasting\PrivateChannel */</span> <span class="k">public</span> <span class="k">function</span> <span class="n">broadcastOn</span><span class="p">():</span> <span class="kt">Channel</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">PrivateChannel</span><span class="p">(</span><span class="s1">'delivery'</span><span class="p">);</span> <span class="p">}</span> <span class="mf">...</span> </code></pre> </div> <p><strong>Step 2:</strong> Instruct the Livewire DeliveryHistory Component to also listen on a Private Channel by changing the value from <code>echo:delivery</code> to <code>echo-private:delivery</code> in the <code>On Attribute</code> like so:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="mf">...</span> <span class="na">#[On('echo-private:delivery,PackageSent')]</span> <span class="k">public</span> <span class="k">function</span> <span class="n">onPackageSent</span><span class="p">(</span><span class="nv">$event</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">packageStatuses</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$event</span><span class="p">;</span> <span class="p">}</span> <span class="mf">...</span> </code></pre> </div> <p>Reload your app, you will see a 403 forbidden error now for WebSocket connections.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1vkz01wjomjcq863if9.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1vkz01wjomjcq863if9.png" alt="403 Forbidden Error"></a><strong>Step 3:</strong> Open up the <code>routes/channels.php</code> file. This is where the authorization logic resides to determine who can listen to a given channel. Replace the code there with the following:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Broadcast</span><span class="p">;</span> <span class="nc">Broadcast</span><span class="o">::</span><span class="nf">channel</span><span class="p">(</span><span class="s1">'delivery'</span><span class="p">,</span> <span class="k">function</span> <span class="p">(</span><span class="nv">$user</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$user</span><span class="o">-&gt;</span><span class="n">id</span> <span class="o">===</span> <span class="mi">1</span><span class="p">;</span> <span class="p">});</span> </code></pre> </div> <p>In the above code, the <strong><code>channel</code></strong> method accepts two arguments: the name of the channel and a callback which returns <strong><code>true</code></strong> or <strong><code>false</code></strong> indicating whether the user is authorized to listen on the channel.</p> <p>Here, we have instructed the app to permit and authorize only a logged-in user with ID 1 to listen to the delivery channel.</p> <p>Now, ensure that the user with ID 1 is logged into the app and check if there's a WebSocket forbidden error.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodwqb3njum8j20d0tuqv.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodwqb3njum8j20d0tuqv.png" alt="Channel authorization"></a>Viola! No error and we can listen on the channel!</p> <p>Try logging with another user and see what happens.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fen2tva1tiiq8rgpa4fx2.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fen2tva1tiiq8rgpa4fx2.png" alt="Channel authentication and authorization"></a>Forbidden! This user is not authorized to listen on this channel.</p> <p>Ideally, in a more robust scenario, each user should be subscribed to their own private channels. This method prevents users from accessing each other's specific events. This is great for game rooms, chat rooms, log history specific boards, etc.</p> <p>So your authorization might need to look like this:</p> <div class="highlight js-code-highlight"> <pre class="highlight php"><code> <span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Broadcast</span><span class="p">;</span> <span class="nc">Broadcast</span><span class="o">::</span><span class="nf">channel</span><span class="p">(</span><span class="s1">'delivery.{id}'</span><span class="p">,</span> <span class="k">function</span> <span class="p">(</span><span class="nv">$user</span><span class="p">,</span> <span class="nv">$id</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="nv">$user</span><span class="o">-&gt;</span><span class="n">id</span> <span class="o">===</span> <span class="nv">$id</span><span class="p">;</span> <span class="p">});</span> </code></pre> </div> <p>This means the following:</p> <ul> <li>Authenticated user with ID 1 can only listen to channel <code>delivery.1</code> </li> <li>Authenticated user with ID 2 can only listen to channel <code>delivery.2</code> </li> <li>Authenticated user with ID 3 can only listen to channel <code>delivery.3</code> </li> </ul> <p>All authorization callbacks receive the currently authenticated user as their first argument and any additional wildcard parameters as their subsequent arguments.</p> <h2> Take It Further! </h2> <p>Laravel Reverb is a fantastic addition to the extensive collection of impressive developer packages in the Laravel ecosystem.</p> <p>In this guide, I invite you to <a href="proxy.php?url=https://github.com/novuhq/laravel-reverb-app" rel="noopener noreferrer">extend the app</a> by adding a persistent layer for delivery statuses. When the event is triggered, it should also be stored in the database.</p> <h2> Next Up: Handle Notifications in Your Laravel Reverb App </h2> <p>I hope you found this guide enjoyable and informative. In the next section, I'll demonstrate how to seamlessly integrate real-time notifications into your Laravel Reverb App.</p> <p>Stay tuned for more, as I'm confident you'll acquire new strategies and abilities that will assist you in building your next app!</p> <p>If you have an idea that requires real-time capabilities, Laravel and Reverb could be the perfect fit. You can find me on <a href="proxy.php?url=https://bit.ly/3vULbrL" rel="noopener noreferrer">Discord</a> and <a href="proxy.php?url=https://bit.ly/44axm4W" rel="noopener noreferrer">Twitter</a>. Don't hesitate to reach out.</p> webdev laravel php notifications