fix: stripe webhooks
fix: add custome created webhook
This commit is contained in:
@@ -13,7 +13,6 @@ use App\Models\Webhook;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Sleep;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class Stripe extends Controller
|
class Stripe extends Controller
|
||||||
@@ -64,22 +63,18 @@ class Stripe extends Controller
|
|||||||
$piData = $stripe->paymentIntents->retrieve($pi, []);
|
$piData = $stripe->paymentIntents->retrieve($pi, []);
|
||||||
$customerId = data_get($piData, 'customer');
|
$customerId = data_get($piData, 'customer');
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||||
if (! $subscription) {
|
|
||||||
Sleep::for(5)->seconds();
|
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
|
||||||
}
|
|
||||||
if (! $subscription) {
|
|
||||||
Sleep::for(5)->seconds();
|
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
|
||||||
}
|
|
||||||
if ($subscription) {
|
if ($subscription) {
|
||||||
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
|
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
|
||||||
$stripe->subscriptions->cancel($subscriptionId, []);
|
$stripe->subscriptions->cancel($subscriptionId, []);
|
||||||
$subscription->update([
|
$subscription->update([
|
||||||
'stripe_invoice_paid' => false,
|
'stripe_invoice_paid' => false,
|
||||||
]);
|
]);
|
||||||
|
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
|
||||||
|
} else {
|
||||||
|
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
|
||||||
|
|
||||||
|
return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
|
||||||
}
|
}
|
||||||
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
|
|
||||||
break;
|
break;
|
||||||
case 'checkout.session.completed':
|
case 'checkout.session.completed':
|
||||||
$clientReferenceId = data_get($data, 'client_reference_id');
|
$clientReferenceId = data_get($data, 'client_reference_id');
|
||||||
@@ -95,7 +90,8 @@ class Stripe extends Controller
|
|||||||
$found = $team->members->where('id', $userId)->first();
|
$found = $team->members->where('id', $userId)->first();
|
||||||
if (! $found->isAdmin()) {
|
if (! $found->isAdmin()) {
|
||||||
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
||||||
throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
|
||||||
|
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
|
||||||
}
|
}
|
||||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||||
if ($subscription) {
|
if ($subscription) {
|
||||||
@@ -123,13 +119,13 @@ class Stripe extends Controller
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||||
if (! $subscription) {
|
if ($subscription) {
|
||||||
Sleep::for(5)->seconds();
|
$subscription->update([
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
'stripe_invoice_paid' => true,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return response("No subscription found for customer: {$customerId}", 400);
|
||||||
}
|
}
|
||||||
$subscription->update([
|
|
||||||
'stripe_invoice_paid' => true,
|
|
||||||
]);
|
|
||||||
break;
|
break;
|
||||||
case 'invoice.payment_failed':
|
case 'invoice.payment_failed':
|
||||||
$customerId = data_get($data, 'customer');
|
$customerId = data_get($data, 'customer');
|
||||||
@@ -167,6 +163,30 @@ class Stripe extends Controller
|
|||||||
}
|
}
|
||||||
send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
||||||
break;
|
break;
|
||||||
|
case 'customer.subscription.created':
|
||||||
|
$customerId = data_get($data, 'customer');
|
||||||
|
$subscriptionId = data_get($data, 'id');
|
||||||
|
$teamId = data_get($data, 'metadata.team_id');
|
||||||
|
$userId = data_get($data, 'metadata.user_id');
|
||||||
|
$team = Team::find($teamId);
|
||||||
|
$found = $team->members->where('id', $userId)->first();
|
||||||
|
if (! $found->isAdmin()) {
|
||||||
|
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
|
||||||
|
|
||||||
|
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
|
||||||
|
}
|
||||||
|
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||||
|
if ($subscription) {
|
||||||
|
return response("Subscription already exists for team: {$teamId}", 400);
|
||||||
|
} else {
|
||||||
|
Subscription::create([
|
||||||
|
'team_id' => $teamId,
|
||||||
|
'stripe_subscription_id' => $subscriptionId,
|
||||||
|
'stripe_customer_id' => $customerId,
|
||||||
|
'stripe_invoice_paid' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'customer.subscription.updated':
|
case 'customer.subscription.updated':
|
||||||
$customerId = data_get($data, 'customer');
|
$customerId = data_get($data, 'customer');
|
||||||
$status = data_get($data, 'status');
|
$status = data_get($data, 'status');
|
||||||
@@ -177,10 +197,6 @@ class Stripe extends Controller
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||||
if (! $subscription) {
|
|
||||||
Sleep::for(5)->seconds();
|
|
||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
|
||||||
}
|
|
||||||
if (! $subscription) {
|
if (! $subscription) {
|
||||||
if ($status === 'incomplete_expired') {
|
if ($status === 'incomplete_expired') {
|
||||||
// send_internal_notification('Subscription incomplete expired for customer: '.$customerId);
|
// send_internal_notification('Subscription incomplete expired for customer: '.$customerId);
|
||||||
@@ -268,7 +284,7 @@ class Stripe extends Controller
|
|||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||||
$team = data_get($subscription, 'team');
|
$team = data_get($subscription, 'team');
|
||||||
if (! $team) {
|
if (! $team) {
|
||||||
throw new Exception('No team found for subscription: '.$subscription->id);
|
return response('No team found for subscription: '.$subscription->id, 400);
|
||||||
}
|
}
|
||||||
SubscriptionTrialEndsSoonJob::dispatch($team);
|
SubscriptionTrialEndsSoonJob::dispatch($team);
|
||||||
break;
|
break;
|
||||||
@@ -277,7 +293,7 @@ class Stripe extends Controller
|
|||||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||||
$team = data_get($subscription, 'team');
|
$team = data_get($subscription, 'team');
|
||||||
if (! $team) {
|
if (! $team) {
|
||||||
throw new Exception('No team found for subscription: '.$subscription->id);
|
return response('No team found for subscription: '.$subscription->id, 400);
|
||||||
}
|
}
|
||||||
$team->trialEnded();
|
$team->trialEnded();
|
||||||
$subscription->update([
|
$subscription->update([
|
||||||
|
@@ -8,31 +8,14 @@ use Stripe\Stripe;
|
|||||||
|
|
||||||
class PricingPlans extends Component
|
class PricingPlans extends Component
|
||||||
{
|
{
|
||||||
public bool $isTrial = false;
|
|
||||||
|
|
||||||
public function mount()
|
|
||||||
{
|
|
||||||
$this->isTrial = ! data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
|
|
||||||
if (config('constants.limits.trial_period') == 0) {
|
|
||||||
$this->isTrial = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function subscribeStripe($type)
|
public function subscribeStripe($type)
|
||||||
{
|
{
|
||||||
$team = currentTeam();
|
|
||||||
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
||||||
|
|
||||||
$priceId = match ($type) {
|
$priceId = match ($type) {
|
||||||
'basic-monthly' => config('subscription.stripe_price_id_basic_monthly'),
|
|
||||||
'basic-yearly' => config('subscription.stripe_price_id_basic_yearly'),
|
|
||||||
'pro-monthly' => config('subscription.stripe_price_id_pro_monthly'),
|
|
||||||
'pro-yearly' => config('subscription.stripe_price_id_pro_yearly'),
|
|
||||||
'ultimate-monthly' => config('subscription.stripe_price_id_ultimate_monthly'),
|
|
||||||
'ultimate-yearly' => config('subscription.stripe_price_id_ultimate_yearly'),
|
|
||||||
'dynamic-monthly' => config('subscription.stripe_price_id_dynamic_monthly'),
|
'dynamic-monthly' => config('subscription.stripe_price_id_dynamic_monthly'),
|
||||||
'dynamic-yearly' => config('subscription.stripe_price_id_dynamic_yearly'),
|
'dynamic-yearly' => config('subscription.stripe_price_id_dynamic_yearly'),
|
||||||
default => config('subscription.stripe_price_id_basic_monthly'),
|
default => config('subscription.stripe_price_id_dynamic_monthly'),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (! $priceId) {
|
if (! $priceId) {
|
||||||
@@ -46,7 +29,11 @@ class PricingPlans extends Component
|
|||||||
'client_reference_id' => auth()->user()->id.':'.currentTeam()->id,
|
'client_reference_id' => auth()->user()->id.':'.currentTeam()->id,
|
||||||
'line_items' => [[
|
'line_items' => [[
|
||||||
'price' => $priceId,
|
'price' => $priceId,
|
||||||
'quantity' => 1,
|
'adjustable_quantity' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'minimum' => 2,
|
||||||
|
],
|
||||||
|
'quantity' => 2,
|
||||||
]],
|
]],
|
||||||
'tax_id_collection' => [
|
'tax_id_collection' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
@@ -54,39 +41,18 @@ class PricingPlans extends Component
|
|||||||
'automatic_tax' => [
|
'automatic_tax' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
],
|
],
|
||||||
|
'subscription_data' => [
|
||||||
|
'metadata' => [
|
||||||
|
'user_id' => auth()->user()->id,
|
||||||
|
'team_id' => currentTeam()->id,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'payment_method_collection' => 'if_required',
|
||||||
'mode' => 'subscription',
|
'mode' => 'subscription',
|
||||||
'success_url' => route('dashboard', ['success' => true]),
|
'success_url' => route('dashboard', ['success' => true]),
|
||||||
'cancel_url' => route('subscription.index', ['cancelled' => true]),
|
'cancel_url' => route('subscription.index', ['cancelled' => true]),
|
||||||
];
|
];
|
||||||
if (str($type)->contains('ultimate')) {
|
|
||||||
$payload['line_items'][0]['adjustable_quantity'] = [
|
|
||||||
'enabled' => true,
|
|
||||||
'minimum' => 10,
|
|
||||||
];
|
|
||||||
$payload['line_items'][0]['quantity'] = 10;
|
|
||||||
}
|
|
||||||
if (str($type)->contains('dynamic')) {
|
|
||||||
$payload['line_items'][0]['adjustable_quantity'] = [
|
|
||||||
'enabled' => true,
|
|
||||||
'minimum' => 2,
|
|
||||||
];
|
|
||||||
$payload['line_items'][0]['quantity'] = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! data_get($team, 'subscription.stripe_trial_already_ended')) {
|
|
||||||
if (config('constants.limits.trial_period') > 0) {
|
|
||||||
$payload['subscription_data'] = [
|
|
||||||
'trial_period_days' => config('constants.limits.trial_period'),
|
|
||||||
'trial_settings' => [
|
|
||||||
'end_behavior' => [
|
|
||||||
'missing_payment_method' => 'cancel',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$payload['payment_method_collection'] = 'if_required';
|
|
||||||
}
|
|
||||||
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
|
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
|
||||||
if ($customer) {
|
if ($customer) {
|
||||||
$payload['customer'] = $customer;
|
$payload['customer'] = $customer;
|
||||||
|
@@ -14,21 +14,18 @@
|
|||||||
:class="selected === 'yearly' ? 'dark:bg-coollabs-100 bg-warning dark:text-white' : ''">
|
:class="selected === 'yearly' ? 'dark:bg-coollabs-100 bg-warning dark:text-white' : ''">
|
||||||
<input type="radio" x-on:click="selected = 'yearly'" name="frequency" value="annually"
|
<input type="radio" x-on:click="selected = 'yearly'" name="frequency" value="annually"
|
||||||
class="sr-only">
|
class="sr-only">
|
||||||
<span>Annually <span class="text-xs dark:text-warning text-coollabs">(save ~20%)</span
|
<span>Annually <span class="text-xs dark:text-warning text-coollabs">(save ~20%)</span></span>
|
||||||
></span>
|
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-root mt-12">
|
<div class="flow-root mt-12">
|
||||||
{{-- <div class="pb-10 text-xl text-center">For the detailed list of features, please visit our landing page: <a
|
|
||||||
class="font-bold underline dark:text-white" href="https://coolify.io">coolify.io</a></div> --}}
|
|
||||||
<div
|
<div
|
||||||
class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-1 lg:divide-x lg:divide-y-0 xl:-mx-4">
|
class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-1 lg:divide-x lg:divide-y-0 xl:-mx-4">
|
||||||
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
||||||
<h3 id="tier-dynamic" class="text-4xl font-semibold leading-7 dark:text-white">Pay-as-you-go</h3>
|
<h3 id="tier-dynamic" class="text-4xl font-semibold leading-7 dark:text-white">Pay-as-you-go</h3>
|
||||||
<p class="mt-4 text-sm leading-6 dark:text-neutral-400">
|
<p class="mt-4 text-sm leading-6 dark:text-neutral-400">
|
||||||
Dynamic pricing based on the number of servers you connect.
|
Dynamic pricing based on the number of servers you connect.
|
||||||
</p>
|
</p>
|
||||||
<p class="flex items-baseline mt-6 gap-x-1">
|
<p class="flex items-baseline mt-6 gap-x-1">
|
||||||
<span x-show="selected === 'monthly'" x-cloak>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
<span class="text-4xl font-bold tracking-tight dark:text-white">$5</span>
|
<span class="text-4xl font-bold tracking-tight dark:text-white">$5</span>
|
||||||
@@ -43,43 +40,38 @@
|
|||||||
<p class="flex items-baseline mb-4 gap-x-1">
|
<p class="flex items-baseline mb-4 gap-x-1">
|
||||||
<span x-show="selected === 'monthly'" x-cloak>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
<span class="text-base font-semibold tracking-tight dark:text-white">$3</span>
|
<span class="text-base font-semibold tracking-tight dark:text-white">$3</span>
|
||||||
<span class="text-sm font-semibold leading-6 "> per additional servers <span class="font-normal dark:text-white">billed monthly (+VAT)</span></span>
|
<span class="text-sm font-semibold leading-6 "> per additional servers <span
|
||||||
|
class="font-normal dark:text-white">billed monthly (+VAT)</span></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span x-show="selected === 'yearly'" x-cloak>
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
<span class="text-base font-semibold tracking-tight dark:text-white">$2.7</span>
|
<span class="text-base font-semibold tracking-tight dark:text-white">$2.7</span>
|
||||||
<span class="text-sm font-semibold leading-6 "> per additional servers <span class="font-normal dark:text-white">billed annually (+VAT)</span></span>
|
<span class="text-sm font-semibold leading-6 "> per additional servers <span
|
||||||
|
class="font-normal dark:text-white">billed annually (+VAT)</span></span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center pt-6">
|
<div class="flex items-center pt-6">
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" class="flex-none w-8 h-8 mr-3 text-warning"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="currentColor" viewBox="0 0 256 256">
|
||||||
class="flex-none w-8 h-8 mr-3 text-warning"
|
<path
|
||||||
fill="currentColor"
|
d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z">
|
||||||
viewBox="0 0 256 256"
|
</path>
|
||||||
><path
|
</svg>
|
||||||
d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="flex flex-col text-sm dark:text-white">
|
<div class="flex flex-col text-sm dark:text-white">
|
||||||
<div>
|
<div>
|
||||||
You need to bring your own servers from any cloud provider (such as <a
|
You need to bring your own servers from any cloud provider (such as <a class="underline"
|
||||||
class="underline"
|
href="https://coolify.io/hetzner" target="_blank">Hetzner</a>, DigitalOcean, AWS,
|
||||||
href="https://coolify.io/hetzner"
|
etc.)
|
||||||
target="_blank">Hetzner</a
|
</div>
|
||||||
>, DigitalOcean, AWS, etc.)
|
<div>
|
||||||
</div>
|
(You can connect your RPi, old laptop, or any other device that runs
|
||||||
<div>
|
the <a class="underline"
|
||||||
(You can connect your RPi, old laptop, or any other device that runs
|
href="https://coolify.io/docs/installation#supported-operating-systems"
|
||||||
the <a
|
target="_blank">supported operating systems</a>.)
|
||||||
class="underline"
|
</div>
|
||||||
href="https://coolify.io/docs/installation#supported-operating-systems"
|
|
||||||
target="_blank">supported operating systems</a
|
|
||||||
>.)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
|
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
|
||||||
class="w-full h-10 buyme" wire:click="subscribeStripe('dynamic-monthly')">
|
class="w-full h-10 buyme" wire:click="subscribeStripe('dynamic-monthly')">
|
||||||
Subscribe
|
Subscribe
|
||||||
@@ -90,120 +82,72 @@
|
|||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
<ul role="list" class="mt-8 space-y-3 text-sm leading-6 dark:text-neutral-400">
|
<ul role="list" class="mt-8 space-y-3 text-sm leading-6 dark:text-neutral-400">
|
||||||
<li class="flex">
|
<li class="flex">
|
||||||
<svg
|
<svg class="flex-none w-5 h-6 mr-3 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
class="flex-none w-5 h-6 mr-3 text-warning"
|
aria-hidden="true">
|
||||||
viewBox="0 0 20 20"
|
<path fill-rule="evenodd"
|
||||||
fill="currentColor"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
aria-hidden="true"
|
clip-rule="evenodd" />
|
||||||
>
|
</svg>
|
||||||
<path
|
Connect
|
||||||
fill-rule="evenodd"
|
<span class="px-1 font-bold dark:text-white">unlimited</span> servers
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Connect
|
|
||||||
<span class="px-1 font-bold dark:text-white">unlimited</span> servers
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex">
|
<li class="flex">
|
||||||
<svg
|
<svg class="flex-none w-5 h-6 mr-3 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
class="flex-none w-5 h-6 mr-3 text-warning"
|
aria-hidden="true">
|
||||||
viewBox="0 0 20 20"
|
<path fill-rule="evenodd"
|
||||||
fill="currentColor"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
aria-hidden="true"
|
clip-rule="evenodd" />
|
||||||
>
|
</svg>
|
||||||
<path
|
Deploy
|
||||||
fill-rule="evenodd"
|
<span class="px-1 font-bold dark:text-white">unlimited</span> applications per server
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Deploy
|
|
||||||
<span class="px-1 font-bold dark:text-white">unlimited</span> applications per server
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-x-3">
|
<li class="flex gap-x-3">
|
||||||
<svg
|
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
class="flex-none w-5 h-6 text-warning"
|
aria-hidden="true">
|
||||||
viewBox="0 0 20 20"
|
<path fill-rule="evenodd"
|
||||||
fill="currentColor"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
aria-hidden="true"
|
clip-rule="evenodd" />
|
||||||
>
|
</svg>
|
||||||
<path
|
Free email notifications
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Free email notifications
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-x-3">
|
<li class="flex gap-x-3">
|
||||||
<svg
|
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
class="flex-none w-5 h-6 text-warning"
|
aria-hidden="true">
|
||||||
viewBox="0 0 20 20"
|
<path fill-rule="evenodd"
|
||||||
fill="currentColor"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
aria-hidden="true"
|
clip-rule="evenodd" />
|
||||||
>
|
</svg>
|
||||||
<path
|
Support by email
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Support by email
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex font-bold dark:text-white gap-x-3">
|
<li class="flex font-bold dark:text-white gap-x-3">
|
||||||
<svg
|
<svg width="512" height="512" class="flex-none w-5 h-6 text-green-500"
|
||||||
width="512"
|
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
height="512"
|
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||||
class="flex-none w-5 h-6 text-green-500"
|
stroke-linejoin="round" stroke-width="2">
|
||||||
viewBox="0 0 24 24"
|
<path
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3-5a9 9 0 0 0 6-8a3 3 0 0 0-3-3a9 9 0 0 0-8 6a6 6 0 0 0-5 3" />
|
||||||
>
|
<path d="M7 14a6 6 0 0 0-3 6a6 6 0 0 0 6-3m4-8a1 1 0 1 0 2 0a1 1 0 1 0-2 0" />
|
||||||
<g
|
</g>
|
||||||
fill="none"
|
</svg>
|
||||||
stroke="currentColor"
|
+ All Upcoming Features
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3-5a9 9 0 0 0 6-8a3 3 0 0 0-3-3a9 9 0 0 0-8 6a6 6 0 0 0-5 3"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M7 14a6 6 0 0 0-3 6a6 6 0 0 0 6-3m4-8a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
+ All Upcoming Features
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex dark:text-white gap-x-3">
|
<li class="flex dark:text-white gap-x-3">
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" class="flex-none w-5 h-6 text-green-500"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 256 256">
|
||||||
class="flex-none w-5 h-6 text-green-500"
|
<rect width="256" height="256" fill="none" />
|
||||||
viewBox="0 0 256 256"
|
<polyline points="32 136 72 136 88 112 120 160 136 136 160 136" fill="none"
|
||||||
><rect width="256" height="256" fill="none" /><polyline
|
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
points="32 136 72 136 88 112 120 160 136 136 160 136"
|
stroke-width="16" />
|
||||||
fill="none"
|
<path
|
||||||
stroke="currentColor"
|
d="M24,104c0-.67,0-1.33,0-2A54,54,0,0,1,78,48c22.59,0,41.94,12.31,50,32,8.06-19.69,27.41-32,50-32a54,54,0,0,1,54,54c0,66-104,122-104,122s-42-22.6-72.58-56"
|
||||||
stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round" stroke-width="16" />
|
||||||
stroke-width="16"
|
</svg>
|
||||||
/><path
|
|
||||||
d="M24,104c0-.67,0-1.33,0-2A54,54,0,0,1,78,48c22.59,0,41.94,12.31,50,32,8.06-19.69,27.41-32,50-32a54,54,0,0,1,54,54c0,66-104,122-104,122s-42-22.6-72.58-56"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="16"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
|
|
||||||
Do you require official support for your self-hosted instance?<a
|
Do you require official support for your self-hosted instance?<a class="underline"
|
||||||
class="underline"
|
href="https://coolify.io/docs/contact">Contact Us</a>
|
||||||
href="https://coolify.io/docs/contact">Contact Us</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user