<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pavel</title>
    <description>The latest articles on DEV Community by Pavel (@pahatrop).</description>
    <link>https://dev.to/pahatrop</link>
    <image>
      <url>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%2F3478909%2F597004e2-3eea-46cb-92d5-e7b481ffaeac.jpeg</url>
      <title>DEV Community: Pavel</title>
      <link>https://dev.to/pahatrop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pahatrop"/>
    <language>en</language>
    <item>
      <title>Testing Stripe Webhooks Locally Without Deploying Your App</title>
      <dc:creator>Pavel</dc:creator>
      <pubDate>Mon, 26 Jan 2026 13:16:15 +0000</pubDate>
      <link>https://dev.to/pahatrop/testing-stripe-webhooks-locally-without-deploying-your-app-2imh</link>
      <guid>https://dev.to/pahatrop/testing-stripe-webhooks-locally-without-deploying-your-app-2imh</guid>
      <description>&lt;p&gt;At some point, every Stripe integration hits the same wall.&lt;/p&gt;

&lt;p&gt;You add webhook handling, everything works in theory — and then you realize that Stripe requires a &lt;strong&gt;public HTTPS endpoint&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Your backend is running locally. Behind NAT. Probably behind a firewall.&lt;/p&gt;

&lt;p&gt;Suddenly, every small change turns into:&lt;br&gt;
deploy → wait → trigger event → read logs → repeat.&lt;/p&gt;

&lt;p&gt;I wanted to keep my backend &lt;strong&gt;local&lt;/strong&gt;, receive &lt;strong&gt;real Stripe events&lt;/strong&gt;, and avoid turning webhook development into a deployment ritual.&lt;/p&gt;

&lt;p&gt;This article shows how I do that.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem with local Stripe development
&lt;/h2&gt;

&lt;p&gt;Stripe webhooks have a few non-negotiable requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public URL&lt;/li&gt;
&lt;li&gt;HTTPS&lt;/li&gt;
&lt;li&gt;stable endpoint&lt;/li&gt;
&lt;li&gt;real event delivery (with signatures)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes sense for production.&lt;br&gt;&lt;br&gt;
It’s much less convenient during development.&lt;/p&gt;

&lt;p&gt;In practice, this often leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pushing half-baked code just to test a webhook&lt;/li&gt;
&lt;li&gt;debugging logic in staging instead of locally&lt;/li&gt;
&lt;li&gt;slowing down iteration for no good reason&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  A real use case: &lt;code&gt;payment_intent.succeeded&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s use a concrete example.&lt;/p&gt;

&lt;p&gt;A typical flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer completes a payment&lt;/li&gt;
&lt;li&gt;Stripe emits &lt;code&gt;payment_intent.succeeded&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend:

&lt;ul&gt;
&lt;li&gt;verifies the webhook signature&lt;/li&gt;
&lt;li&gt;updates internal state&lt;/li&gt;
&lt;li&gt;triggers business logic&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This logic is often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stateful&lt;/li&gt;
&lt;li&gt;timing-sensitive&lt;/li&gt;
&lt;li&gt;annoying to debug without real events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mocks help, but they don’t fully replace real Stripe payloads.&lt;/p&gt;


&lt;h2&gt;
  
  
  A minimal NestJS webhook handler
&lt;/h2&gt;

&lt;p&gt;Here’s a stripped-down NestJS example that handles a Stripe webhook properly.&lt;/p&gt;

&lt;p&gt;Stripe requires access to the &lt;strong&gt;raw request body&lt;/strong&gt;, so this matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webhooks/stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handleStripeWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_intent.succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// handle successful payment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, everything works — except Stripe can’t reach your machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why localhost is not enough
&lt;/h2&gt;

&lt;p&gt;Stripe sends webhooks &lt;strong&gt;from their servers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your local server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lives behind NAT&lt;/li&gt;
&lt;li&gt;has no public IP&lt;/li&gt;
&lt;li&gt;often can’t accept inbound connections at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Port forwarding might work at home, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it’s not always possible&lt;/li&gt;
&lt;li&gt;it’s rarely acceptable on corporate networks&lt;/li&gt;
&lt;li&gt;it’s not something you want to rely on daily&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common workarounds (and why they fall short)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deploying to staging
&lt;/h3&gt;

&lt;p&gt;Works, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow feedback loop&lt;/li&gt;
&lt;li&gt;noisy logs&lt;/li&gt;
&lt;li&gt;unnecessary infrastructure work&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;stripe listen&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stripe provides an official CLI command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:3000/webhooks/stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s useful, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it does &lt;strong&gt;not&lt;/strong&gt; use a real public HTTPS endpoint&lt;/li&gt;
&lt;li&gt;it relies on an outbound tunnel initiated by the CLI&lt;/li&gt;
&lt;li&gt;it doesn’t reflect how webhooks behave in production&lt;/li&gt;
&lt;li&gt;it doesn’t help with other services that require HTTPS callbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good for quick tests, not great for realistic debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  ngrok
&lt;/h3&gt;

&lt;p&gt;Also works, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URLs change&lt;/li&gt;
&lt;li&gt;requires extra setup&lt;/li&gt;
&lt;li&gt;can be blocked or restricted in some regions or networks&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What we actually want
&lt;/h2&gt;

&lt;p&gt;For local webhook development, the requirements are simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;real HTTPS endpoint&lt;/li&gt;
&lt;li&gt;stable public URL&lt;/li&gt;
&lt;li&gt;no DNS configuration&lt;/li&gt;
&lt;li&gt;no certificate management&lt;/li&gt;
&lt;li&gt;no redeploys&lt;/li&gt;
&lt;li&gt;backend keeps running locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;br&gt;
&lt;strong&gt;Stripe should think it’s talking to a production server — while everything stays local.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Exposing a local NestJS server via HTTPS
&lt;/h2&gt;

&lt;p&gt;The approach is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run your NestJS app locally&lt;/li&gt;
&lt;li&gt;Expose it through a secure HTTPS tunnel&lt;/li&gt;
&lt;li&gt;Forward requests to &lt;code&gt;localhost&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a tunnel service, this can be done in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx start-pnode &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a public HTTPS URL&lt;/li&gt;
&lt;li&gt;automatic TLS&lt;/li&gt;
&lt;li&gt;stable endpoint&lt;/li&gt;
&lt;li&gt;traffic forwarded to your local server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your application code doesn’t change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting Stripe to the local endpoint
&lt;/h2&gt;

&lt;p&gt;Now the fun part.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the Stripe Dashboard&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Developers → Webhooks&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add a new endpoint&lt;/li&gt;
&lt;li&gt;Paste your public HTTPS URL:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://your-subdomain.pnode.site/webhooks/stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Select events (e.g. &lt;code&gt;payment_intent.succeeded&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Copy the signing secret&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it. Stripe is now talking directly to your local machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-end test
&lt;/h2&gt;

&lt;p&gt;Trigger a test event from the Stripe dashboard.&lt;/p&gt;

&lt;p&gt;On your local machine, you’ll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a real HTTPS request&lt;/li&gt;
&lt;li&gt;a real Stripe payload&lt;/li&gt;
&lt;li&gt;a valid signature&lt;/li&gt;
&lt;li&gt;your NestJS handler executing normally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No staging environment.&lt;br&gt;
No redeploy.&lt;br&gt;
No guessing.&lt;/p&gt;

&lt;p&gt;This is the point where webhook development stops being painful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security notes
&lt;/h2&gt;

&lt;p&gt;A few important reminders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always verify the webhook signature&lt;/li&gt;
&lt;li&gt;Don’t rely on IP allowlists alone&lt;/li&gt;
&lt;li&gt;Treat exposed endpoints as temporary&lt;/li&gt;
&lt;li&gt;Don’t use this setup as-is for production traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach is about &lt;strong&gt;local development speed&lt;/strong&gt;, not replacing production infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this approach works best
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Webhook-heavy backends&lt;/li&gt;
&lt;li&gt;Early-stage products&lt;/li&gt;
&lt;li&gt;Rapid iteration on payment logic&lt;/li&gt;
&lt;li&gt;Debugging real Stripe edge cases locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workflow involves frequent webhook changes, keeping everything local is a big win.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Stripe webhooks don’t require you to deploy on every change — they just require HTTPS.&lt;/p&gt;

&lt;p&gt;Once you remove the friction around that, local development becomes fast again.&lt;/p&gt;

&lt;p&gt;And debugging payments where your code actually runs turns out to be much more pleasant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.stripe.com" rel="noopener noreferrer"&gt;Stripe documentation&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pnode.site/docs" rel="noopener noreferrer"&gt;PNode documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>webdev</category>
      <category>stripe</category>
      <category>node</category>
    </item>
    <item>
      <title>Forget Router Configs and VPNs: How to Show a Local Site to a Client in 5 Minutes</title>
      <dc:creator>Pavel</dc:creator>
      <pubDate>Sun, 05 Oct 2025 09:31:05 +0000</pubDate>
      <link>https://dev.to/pahatrop/forget-router-configs-and-vpns-how-to-show-a-local-site-to-a-client-in-5-minutes-g8j</link>
      <guid>https://dev.to/pahatrop/forget-router-configs-and-vpns-how-to-show-a-local-site-to-a-client-in-5-minutes-g8j</guid>
      <description>&lt;h2&gt;
  
  
  Forget Router Configs and VPNs: How to Show a Local Site to a Client in 5 Minutes
&lt;/h2&gt;

&lt;p&gt;Familiar situation?&lt;/p&gt;

&lt;p&gt;You’ve been developing a web application locally for weeks.&lt;br&gt;
Demo time comes - and suddenly you hit a wall:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you quickly and safely show a localhost app to someone outside your network?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The usual options are rarely pleasant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;“Just connect to my VPN”&lt;/strong&gt; - essentially inviting someone into your private network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“I’ll forward a port on my router”&lt;/strong&gt; - 40 minutes lost to NAT, firewalls, and ISP quirks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Let me spin up a VPS real quick”&lt;/strong&gt; - extra cost, deployment time, and configuration overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if your ISP doesn’t provide a static IP address, the situation gets even worse.&lt;/p&gt;

&lt;p&gt;For a long time, I used tools like ngrok or localtunnel.&lt;br&gt;
They helped, but speed, stability, and availability were inconsistent.&lt;/p&gt;

&lt;p&gt;In this article, I want to show how &lt;strong&gt;PNode&lt;/strong&gt; solves this problem with a much simpler model:&lt;br&gt;
a secure public URL that forwards traffic directly to your local service - no network setup, no servers, no certificates.&lt;/p&gt;

&lt;p&gt;Let’s walk through a real-world example.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Scenario
&lt;/h2&gt;

&lt;p&gt;You’re a freelancer.&lt;br&gt;
A client asks to see a working prototype &lt;strong&gt;right now&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The app already runs locally. You just need to expose it to the Internet.&lt;/p&gt;


&lt;h2&gt;
  
  
  Option 1: Instant Demo with Anonymous Mode
&lt;/h2&gt;

&lt;p&gt;PNode supports an &lt;strong&gt;Anonymous mode&lt;/strong&gt; that requires &lt;strong&gt;no registration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You don’t create an account, don’t generate tokens, don’t configure anything.&lt;/p&gt;

&lt;p&gt;You simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx start-pnode@latest &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;PNode will assign a temporary public domain in the form:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tmp-xxxxx.pnode.site&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What you get
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A public HTTPS URL&lt;/li&gt;
&lt;li&gt;Traffic forwarded to your local port&lt;/li&gt;
&lt;li&gt;No credentials, no setup&lt;/li&gt;
&lt;li&gt;The domain is valid for &lt;strong&gt;12 hours&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;After expiration, a new temporary domain is issued automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mode is ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;quick client demos&lt;/li&gt;
&lt;li&gt;webhook testing&lt;/li&gt;
&lt;li&gt;temporary previews&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only limitation is that anonymous mode does &lt;strong&gt;not&lt;/strong&gt; support frontend / backend separation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 2: Fixed Domain with Authorized Mode
&lt;/h2&gt;

&lt;p&gt;If you need a stable URL or more control, PNode offers &lt;strong&gt;Authorized mode&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create an Account and Project
&lt;/h3&gt;

&lt;p&gt;Go to &lt;a href="https://pnode.site" rel="noopener noreferrer"&gt;pnode.site&lt;/a&gt; and sign up.&lt;/p&gt;

&lt;p&gt;After logging in, create a project.&lt;br&gt;
Each project corresponds to &lt;strong&gt;one fixed public domain&lt;/strong&gt;, for example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pahatrop.pnode.site&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The project page gives you a single credential:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AGENT_TOKEN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This token uniquely identifies your project.&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 2: Prepare a Local Application
&lt;/h3&gt;

&lt;p&gt;In my case, I’m running a simple NestJS API locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nest new my-demo-server
&lt;span class="nb"&gt;cd &lt;/span&gt;my-demo-server
npm run start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server listens on:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At this point, it’s still completely inaccessible from the outside.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Run the Agent
&lt;/h3&gt;

&lt;p&gt;Set the environment variables (on Windows, use &lt;code&gt;set&lt;/code&gt; instead of &lt;code&gt;export&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-agent-token
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LOCAL_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx start-pnode@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can pass everything via CLI flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx start-pnode@latest &lt;span class="nt"&gt;--token&lt;/span&gt; your-agent-token &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Done
&lt;/h2&gt;

&lt;p&gt;Once the agent connects, your local service becomes publicly accessible.&lt;/p&gt;

&lt;p&gt;You send the link:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pahatrop.pnode.site&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The client opens it and immediately sees the application running on your machine.&lt;/p&gt;

&lt;p&gt;You can keep coding, refreshing, and changing things in real time - no redeployments required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frontend and Backend Separation (Authorized Mode)
&lt;/h2&gt;

&lt;p&gt;Authorized projects can optionally enable domain separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pahatrop.pnode.site&lt;/code&gt; - static frontend&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api-pahatrop.pnode.site&lt;/code&gt; - backend proxied to your local server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frontend builds can be uploaded as a ZIP archive or deployed from a connected Git repository.&lt;/p&gt;

&lt;p&gt;This setup works well for SPA + API projects where the backend is still under active development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why PNode?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast setup&lt;/strong&gt; - from zero to public URL in minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No network configuration&lt;/strong&gt; - no NAT, no firewall rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure by default&lt;/strong&gt; - encrypted traffic between agent and PNode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No static IP required&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two clear modes&lt;/strong&gt; - anonymous for speed, authorized for stability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PNode is not just another tunneling tool.&lt;br&gt;
It solves a very practical problem: &lt;strong&gt;sharing local work quickly and professionally, without infrastructure overhead&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you regularly demo prototypes, test integrations, or expose local services, this tool fits naturally into that workflow.&lt;/p&gt;

&lt;p&gt;What tools do you use to expose local services?&lt;br&gt;
Feel free to share your experience in the comments.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>tooling</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
