Skip to content
Written with Claude
IMPORTANT

As you may notice, this page and pretty much the entire website were obviously created with the help of AI. I wonder how you could tell? Was it a big "Written With Claude" badge on every page? I moved it to the top now (with the help of AI of course) to make it even more obvious. There are a few blogposts that were written by me manually, the old-fashioned way, I hope there will be more in the future, and those have a similar "Human Written" badge. This project (not the website), on the other hand, is a very, very different story. It took me more than two years of painstaking and unpaid work in my own free time. A story that, hopefully, I will tell someday. But meanwhile, what would you like me to do? To create a complex documentation website with a bunch of highly technical articles with the help of AI and fake it, to give you an illusion that I also did that manually? Like the half of itnernet is doing at this point? How does that makes any sense? Is that even fair to you? Or maybe to create this website manually, the old-fashioned way, just for you? While working a paid job for a salary, most of you wouldn't even get up in the morning. Would you like me to sing you a song while we're at it? For your personal entertainment? Seriously, get a grip. Do you find this information less valuable because of the way this website was created? I give my best to fix it to keep the information as accurate as possible, and I think it is very accurate at this point. If you find some mistakes, inaccurancies or problems, there is a comment section at the bottom of every page, which I also made with the help of the AI. And I woould very much appreciate if you leave your feedback there. Look, I'm just a guy who likes SQL, that's all. If you don't approve of how this website was constructed and the use of AI tools, I suggest closing this page and never wever coming back. And good riddance. And I would ban your access if I could know how. Thank you for your attention to this matter.

PROXY_OUT

Also known as

forward_proxy (with or without @ prefix)

Available since version 3.11.0

Execute the PostgreSQL function first, then forward its result body to an upstream service. The upstream response is returned to the client.

Syntax

code
@proxy_out
@proxy_out [ host_url ]
@proxy_out [ http_method ]
@proxy_out [ http_method ] [ host_url ]

Description

The proxy_out annotation reverses the flow of the existing proxy annotation. Instead of forwarding the incoming request to upstream, proxy_out forwards the outgoing function result.

This enables a common pattern where business logic in PostgreSQL prepares a payload, and an external service performs processing that PostgreSQL cannot do — PDF rendering, image processing, ML inference, email sending, etc.

code
Client Request → NpgsqlRest
  → Execute PostgreSQL function
  → Forward function result as request body to upstream service
  → Forward original query string to upstream URL
  → Return upstream response to client

The client-facing HTTP method and the upstream HTTP method are independent — the client can send a GET while the upstream receives a POST.

Basic Usage

sql
sql
create function generate_report(report_id int)
returns json
language plpgsql as $$
begin
    return json_build_object(
        'title', 'Monthly Report',
        'data', (select json_agg(row_to_json(t)) from sales t where month = report_id)
    );
end;
$$;

comment on function generate_report(int) is 'HTTP GET
@proxy_out POST https://render-service.internal/render';

The client calls GET /api/generate-report/?reportId=3. The server:

  1. Executes generate_report(3) in PostgreSQL.
  2. Takes the returned JSON and POSTs it to https://render-service.internal/render/api/generate-report/?reportId=3 (original query string forwarded).
  3. Returns the upstream response (e.g., a rendered PDF) directly to the client with the upstream's content-type and status code.

Proxy Annotations

Basic Proxy Out with Default Host

Uses the host from ProxyOptions.Host configuration:

sql
sql
comment on function my_func() is '@proxy_out';

Proxy Out with Custom Host

Override the default host:

sql
sql
comment on function my_func() is 'HTTP GET
@proxy_out POST https://my-other-service.internal';

Proxy Out with HTTP Method Override

Specify which HTTP method to use for the upstream request (uses default host from configuration):

sql
sql
comment on function my_func() is 'HTTP GET
@proxy_out PUT';

The client sends GET, but the upstream receives PUT with the function's result as the body.

Combined Method and Host

Specify both HTTP method and host:

sql
sql
comment on function my_func() is 'HTTP GET
@proxy_out POST https://render-service.internal/render';

Self-Referencing Proxy Out (Relative Path)

Use a relative path starting with / to forward the function result to another endpoint on the same server:

sql
sql
comment on function my_func() is 'HTTP GET
@proxy_out POST /api/internal-processor';

Self-referencing calls bypass the HTTP stack entirely — the target endpoint handler is invoked directly in-process with zero network overhead.

URL Resolution

The target URL follows the same resolution rules as proxy:

  • Annotation URL takes priority — if provided (absolute or relative), the global ProxyOptions.Host is ignored.
  • Global ProxyOptions.Host is used only when the annotation has no URL.
  • A relative path (starting with /) always creates an internal self-call, regardless of ProxyOptions.Host.

Query String Forwarding

The original client query string is forwarded to the upstream service as-is. This allows the upstream to receive the same parameters that were used to invoke the function:

sql
sql
create function generate_report(p_format text, p_id int)
returns json
language plpgsql as $$
begin
    return json_build_object('id', p_id, 'data', 'report');
end;
$$;

comment on function generate_report(text, int) is 'HTTP GET
@proxy_out POST';

Calling GET /api/generate-report/?pFormat=pdf&pId=123 executes the function, then POSTs the result to the upstream with ?pFormat=pdf&pId=123 appended to the URL.

Error Handling

  • If the function fails (database error, exception), the error is returned directly to the client — the proxy call is never made.
  • If the upstream fails (5xx, timeout, connection error), the upstream's error status and body are forwarded to the client (502 for connection errors, 504 for timeouts).

Examples

PDF Rendering Pipeline

Prepare data in PostgreSQL and render it as a PDF via an external service:

sql
sql
create function invoice_pdf(invoice_id int)
returns json
language plpgsql as $$
begin
    return json_build_object(
        'invoice_number', invoice_id,
        'items', (select json_agg(row_to_json(i)) from invoice_items i where i.invoice_id = invoice_pdf.invoice_id),
        'total', (select sum(amount) from invoice_items where invoice_items.invoice_id = invoice_pdf.invoice_id)
    );
end;
$$;

comment on function invoice_pdf(int) is 'HTTP GET
@proxy_out POST https://pdf-service.internal/render';

ML Inference

Send prepared feature data to an ML service:

sql
sql
create function predict_churn(customer_id int)
returns json
language plpgsql as $$
begin
    return (
        select json_build_object(
            'features', json_build_object(
                'total_orders', count(*),
                'last_order_days', extract(day from now() - max(order_date)),
                'avg_order_value', avg(total)
            )
        )
        from orders
        where orders.customer_id = predict_churn.customer_id
    );
end;
$$;

comment on function predict_churn(int) is 'HTTP GET
@proxy_out POST https://ml-service.internal/predict/churn';

Email Sending

Prepare email content in PostgreSQL and send via an email service:

sql
sql
create function send_welcome_email(user_id int)
returns json
language plpgsql as $$
declare
    u record;
begin
    select * into u from users where id = user_id;
    return json_build_object(
        'to', u.email,
        'subject', 'Welcome to Our Platform',
        'body', format('Hello %s, welcome!', u.display_name)
    );
end;
$$;

comment on function send_welcome_email(int) is 'HTTP POST
@proxy_out POST https://email-service.internal/send';

TypeScript Client

The TypeScript client generator (NpgsqlRest.TsClient) recognizes proxy_out endpoints and generates functions that return the raw Response object. Since the actual response comes from the upstream proxy service (not from the PostgreSQL function's return type), the generated function returns Promise<Response>:

typescript
typescript
// Generated for a proxy_out endpoint
export async function generateReport() : Promise<Response> {
    const response = await fetch(baseUrl + "/api/generate-report", {
        method: "GET",
    });
    return response;
}

This allows the caller to handle the response appropriately (.json(), .blob(), .text(), etc.).

Configuration

Uses the same ProxyOptions configuration as the existing proxy annotation. ProxyOptions.Enabled must be true:

json
json
{
  "NpgsqlRest": {
    "ProxyOptions": {
      "Enabled": true,
      "Host": "https://api.example.com",
      "DefaultTimeout": "30 seconds"
    }
  }
}

See Proxy Options for complete configuration reference.

  • PROXY - Forward incoming requests to upstream (reverse proxy)
  • Proxy Options - Proxy configuration reference
  • HTTP - Expose function as HTTP endpoint

See Also

Comments