s.plan_type IN (NULL, 'basic') is just shorthand for (s.plan_type = NULL OR s.plan_type = 'basic').
Anything = NULL always returns NULL, not true. And only true qualifies in a WHERE clause. So rephrase:
SELECT u.id, s.plan_type
FROM users u
LEFT JOIN subscriptions s ON s.user_id = u.id
WHERE (s.plan_type = 'basic' OR
s.plan_type IS NULL);
To be precise, that's only correct if subscriptions.plan_type is defined NOT NULL. Failing that, the resulting NULL value could be just that, not a missing plan.
This alternative formulation does not share the same ambiguity for a resulting NULL:
SELECT u.id, s.plan_type
FROM users u
JOIN subscriptions s ON s.user_id = u.id
WHERE s.plan_type = 'basic'
UNION ALL
SELECT u.id, NULL -- always means missing
FROM users u
WHERE NOT EXISTS (SELECT FROM subscriptions WHERE user_id = u.id);
And it may be faster, because Postgres (like most RDBMS) often doesn't perform well with OR'ed predicates. See about "ugly OR":
If referential integrity is guaranteed with a FK constraint from subscriptions.user_id to users.id, and s.user_id is defined NOT NULL you can omit the users table from the first SELECT:
SELECT user_id AS id, plan_type
FROM subscriptions
WHERE plan_type = 'basic'
UNION ALL
SELECT id, NULL
FROM users u
WHERE NOT EXISTS (SELECT FROM subscriptions WHERE user_id = u.id);
Might be fastest.
Aside: No array is involved here. You show the form of IN using a list. See: